123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723 |
- // Copyright © 2019 - 2020 Oscar Campos <oscar.campos@thepimpam.com>
- // Copyright © 2017 - William Edwards
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License
- package gdnative
- import (
- "bytes"
- "fmt"
- "go/ast"
- "go/printer"
- "go/token"
- "os"
- "reflect"
- "strings"
- )
- const (
- godotRegister string = "godot::register"
- godotConstructor string = "godot::constructor"
- godotDestructor string = "godot::destructor"
- godotExport string = "godot::export"
- )
- // LookupRegistrableTypeDeclarations parses the given package AST and adds any
- // relevant data to the registry so we can generate boilerplate registration code
- func LookupRegistrableTypeDeclarations(pkg *ast.Package) map[string]Registrable {
- var classes = make(map[string]Registrable)
- // make a first iteration to capture all registrable classes and their properties
- for _, file := range pkg.Files {
- for _, node := range file.Decls {
- gd, ok := node.(*ast.GenDecl)
- if !ok {
- continue
- }
- for _, d := range gd.Specs {
- tp, ok := d.(*ast.TypeSpec)
- if !ok {
- continue
- }
- sp, ok := tp.Type.(*ast.StructType)
- if !ok {
- continue
- }
- if gd.Doc != nil {
- for _, line := range gd.Doc.List {
- original := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
- docstring := strings.ToLower(original)
- if strings.HasPrefix(docstring, godotRegister) {
- className := getClassName(tp)
- class := registryClass{
- base: getBaseClassName(sp),
- }
- classes[className] = &class
- // set alias if defined
- if strings.Contains(docstring, " as ") {
- class.alias = strings.TrimSpace(strings.Split(strings.Split(original, " as ")[1], " ")[0])
- }
- }
- }
- }
- }
- }
- }
- // make a second iteration to look for class methods and signals
- for className := range classes {
- class := classes[className]
- for _, file := range pkg.Files {
- class.SetConstructor(lookupInstanceCreateFunc(className, file))
- class.SetDestructor(lookupInstanceDestroyFunc(className, file))
- class.AddMethods(lookupMethods(className, file))
- class.AddSignals(lookupSignals(className, file))
- class.AddProperties(lookupProperties(className, file))
- }
- }
- return classes
- }
- // getClassName extracts and build the right class name for the registry
- func getClassName(tp *ast.TypeSpec) string {
- className := tp.Name.String()
- return className
- }
- // getBaseClassName extracts the base class name for the registry
- func getBaseClassName(sp *ast.StructType) string {
- // TODO: need to make this way smarter to look for parent types of this type
- var baseClassName string
- for i := 0; i < sp.Fields.NumFields()-1; i++ {
- expr, ok := sp.Fields.List[i].Type.(*ast.SelectorExpr)
- if !ok {
- continue
- }
- ident, ok := expr.X.(*ast.Ident)
- if !ok {
- continue
- }
- if ident.Name != "godot" {
- continue
- }
- baseClassName = fmt.Sprintf("godot.%s", expr.Sel.Name)
- break
- }
- return baseClassName
- }
- // lookupInstanceCreateFunc extract the "constructor" for the given type or create a default one
- func lookupInstanceCreateFunc(className string, file *ast.File) *registryConstructor {
- for _, node := range file.Decls {
- fd, ok := node.(*ast.FuncDecl)
- if !ok || fd.Doc == nil {
- continue
- }
- for _, line := range fd.Doc.List {
- docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
- if strings.HasPrefix(strings.ToLower(docstring), godotConstructor) {
- structName := strings.TrimSpace(docstring[len(godotConstructor):])
- // get rid of parenthesis
- if strings.HasPrefix(structName, "(") && strings.HasSuffix(structName, ")") {
- // make sure this is the only parenthesis structName
- if strings.Count(structName, "(") > 1 || strings.Count(structName, ")") > 1 {
- // this is a syntax error
- fmt.Printf("could not parse constructor comment %s, many parenthesis", docstring)
- }
- structName = structName[1 : len(structName)-1]
- if structName != className {
- // this constructor doesn't match with our class, skip it
- continue
- }
- constructor, err := validateConstructor(structName, fd)
- if err != nil {
- panic(err)
- }
- return constructor
- }
- }
- }
- }
- // if we are here it means the user didn't specified a custom constructor
- return nil
- }
- // validateConstructor returns an error if the given constructor is not valid, it returns nil otherwise
- func validateConstructor(structName string, fd *ast.FuncDecl) (*registryConstructor, error) {
- funcName := fd.Name.String()
- if fd.Recv != nil {
- value := "UnknownType"
- switch t := fd.Recv.List[0].Type.(type) {
- case *ast.StarExpr:
- value = t.X.(*ast.Ident).Name
- case *ast.Ident:
- value = t.Name
- }
- return nil, fmt.Errorf("%s is a method of %s type it can not be used as constructor", funcName, value)
- }
- if fd.Type.Params.List != nil {
- return nil, fmt.Errorf("constructors of %s values take no params but %s takes %d", structName, funcName, fd.Type.Params.NumFields())
- }
- if fd.Type.Results == nil || fd.Type.Results.List == nil {
- return nil, fmt.Errorf("constructors of %s values have to return a pointer to *%s but %s return nothing", structName, structName, funcName)
- }
- if fd.Type.Results.NumFields() > 1 {
- return nil, fmt.Errorf("constructors of %s values must return exactly one value but %s returns %d", structName, funcName, fd.Type.Results.NumFields())
- }
- switch t := fd.Type.Results.List[0].Type.(type) {
- case *ast.StarExpr:
- if t.X.(*ast.Ident).Name != structName {
- return nil, fmt.Errorf(
- "constructors of %s values must return a pointer to *%s but %s returns a pointer to %s instead",
- structName, structName, funcName, t.X.(*ast.Ident).Name,
- )
- }
- default:
- return nil, fmt.Errorf("constructors of %s values must return a pointer to *%s but %s returns %v", structName, structName, funcName, t)
- }
- constructor := registryConstructor{
- class: structName,
- customFunc: fd.Name.String(),
- }
- return &constructor, nil
- }
- // lookupInstanceDestroyFunc
- func lookupInstanceDestroyFunc(className string, file *ast.File) *registryDestructor {
- for _, node := range file.Decls {
- fd, ok := node.(*ast.FuncDecl)
- if !ok || fd.Doc == nil {
- continue
- }
- for _, line := range fd.Doc.List {
- docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
- if strings.HasPrefix(strings.ToLower(docstring), godotDestructor) {
- structName := strings.TrimSpace(docstring[len(godotDestructor):])
- // get rid of parenthesis
- if strings.HasPrefix(structName, "(") && strings.HasSuffix(structName, ")") {
- // make sure this is the only parenthesis structName
- if strings.Count(structName, "(") > 1 || strings.Count(structName, ")") > 1 {
- // this is a syntax error
- fmt.Printf("could not parse destructor comment %s, many parenthesis", docstring)
- os.Exit(1)
- }
- structName = structName[1 : len(structName)-1]
- if structName != className {
- // this destructor doesn't match with our class, skip it
- continue
- }
- destructor, err := validateDestructor(structName, fd)
- if err != nil {
- fmt.Printf("could not validate destructor: %s", err)
- os.Exit(1)
- }
- return destructor
- }
- }
- }
- }
- // if we are here that means the user didn't provide a custom destructor function
- return nil
- }
- // validateDestructor returns an error if the given constructor is not valid, it returns nil otherwise
- func validateDestructor(structName string, fd *ast.FuncDecl) (*registryDestructor, error) {
- funcName := fd.Name.String()
- if fd.Recv != nil {
- value := "UnknownType"
- switch t := fd.Recv.List[0].Type.(type) {
- case *ast.StarExpr:
- value = t.X.(*ast.Ident).Name
- case *ast.Ident:
- value = t.Name
- }
- return nil, fmt.Errorf("%s is a method of %s type it can not be used as destructor", funcName, value)
- }
- if fd.Type.Params.List != nil {
- return nil, fmt.Errorf("destructors of %s values take no params but %s takes %d", structName, funcName, fd.Type.Params.NumFields())
- }
- if fd.Type.Results != nil || fd.Type.Results.List != nil || len(fd.Type.Results.List) > 0 {
- return nil, fmt.Errorf("destructors of %s values have to return a pointer to *%s but %s return nothing", structName, structName, funcName)
- }
- destructor := registryDestructor{
- class: structName,
- customFunc: funcName,
- }
- return &destructor, nil
- }
- // lookupMethods look up for every exported method that is owned by the type
- // and fill a registration data structure with it
- func lookupMethods(className string, file *ast.File) []*registryMethod {
- methods := []*registryMethod{}
- for _, node := range file.Decls {
- fd, ok := node.(*ast.FuncDecl)
- if !ok {
- continue
- }
- // check for init function, if present fail and complain
- if fd.Name.String() == "init" {
- fmt.Printf(
- "init function present on files, you can not provide your own init function while autoregistering classes",
- )
- os.Exit(1)
- }
- // ignore non methods
- if fd.Recv == nil {
- continue
- }
- alias, exported := extractExportedAndAliasFromDoc(fd.Doc)
- // ignore non exported methods
- if !fd.Name.IsExported() && !exported {
- continue
- }
- // ignore methods from other types
- switch t := fd.Recv.List[0].Type.(type) {
- case *ast.StarExpr:
- if t.X.(*ast.Ident).Name != className {
- continue
- }
- case *ast.Ident:
- if t.Name != className {
- continue
- }
- default:
- // this should not be possible but just in case
- continue
- }
- funcName := fd.Name.String()
- method := registryMethod{
- name: funcName,
- alias: alias,
- class: className,
- params: lookupParams(fd.Type.Params),
- returnValues: lookupReturnValues(fd),
- }
- methods = append(methods, &method)
- }
- return methods
- }
- // lookupSignals look up for every signal that is owned by the type and fill
- // a registration data structure with it
- func lookupSignals(className string, file ast.Node) []*registrySignal {
- signals := []*registrySignal{}
- ast.Inspect(file, func(node ast.Node) (cont bool) {
- cont = true
- _, ok := node.(*ast.StructType)
- if ok {
- return
- }
- cl, ok := node.(*ast.CompositeLit)
- if !ok {
- return
- }
- st, ok := cl.Type.(*ast.SelectorExpr)
- if !ok {
- return
- }
- if st.X.(*ast.Ident).Name == "gdnative" && st.Sel.Name == "Signal" {
- signal := registrySignal{}
- for i := range cl.Elts {
- kv, ok := cl.Elts[i].(*ast.KeyValueExpr)
- if !ok {
- continue
- }
- key := kv.Key.(*ast.Ident).Name
- switch key {
- case "Name":
- signal.name = kv.Value.(*ast.BasicLit).Value
- case "Args":
- arguments, ok := kv.Value.(*ast.CompositeLit)
- if !ok {
- fmt.Printf(
- "WARNING: arguments on signal %s has the wrong type, it should be *ast.CompositeLit, arguments will be ignored\n",
- signal.name,
- )
- signal.args = "[]*gdnative.SignalArgument{}"
- continue
- }
- signal.args = parseSignalArgs(arguments)
- case "DefaultArgs":
- defaultArguments, ok := kv.Value.(*ast.CompositeLit)
- if !ok {
- fmt.Printf(
- "WARNING: default arguments on signal %s has the wrong type, it should be *ast.CompositeLit, arguments will be ignored\n",
- signal.name,
- )
- signal.defaults = "[]*gdnative.Variant{}"
- continue
- }
- signal.defaults = parseSignalArgs(defaultArguments)
- }
- }
- signals = append(signals, &signal)
- }
- return
- })
- return signals
- }
- func lookupParams(fields *ast.FieldList) []*registryMethodParam {
- params := []*registryMethodParam{}
- if fields.NumFields() > 0 {
- for _, field := range fields.List {
- kind := parseDefault(field.Type, "")
- switch kind {
- case "":
- // if we don't have a type skip iteration
- continue
- }
- for _, name := range field.Names {
- params = append(params, ®istryMethodParam{
- name: name.String(),
- kind: kind,
- })
- }
- }
- }
- return params
- }
- func lookupReturnValues(fd *ast.FuncDecl) []*registryMethodReturnValue {
- returnValues := []*registryMethodReturnValue{}
- if fd.Type.Results.NumFields() > 0 {
- for _, result := range fd.Type.Results.List {
- value := registryMethodReturnValue{
- kind: parseDefault(result.Type, "gdnative.Variant"),
- }
- returnValues = append(returnValues, &value)
- }
- }
- return returnValues
- }
- func lookupProperties(className string, file *ast.File) []*registryProperty {
- properties := []*registryProperty{}
- for _, node := range file.Decls {
- gd, ok := node.(*ast.GenDecl)
- if !ok {
- continue
- }
- for _, d := range gd.Specs {
- tp, ok := d.(*ast.TypeSpec)
- if !ok {
- continue
- }
- sp, ok := tp.Type.(*ast.StructType)
- if !ok {
- continue
- }
- if getClassName(tp) != className {
- continue
- }
- if sp.Fields.NumFields() > 0 {
- for _, field := range sp.Fields.List {
- if field.Names == nil {
- // this field is a selector expression, skip it
- continue
- }
- gdnativeKind := ""
- kind := parseDefault(field.Type, "")
- switch kind {
- case "":
- // we don't have a type skip iteration
- continue
- case "gdnative.Signal":
- // this is a signal skip it
- continue
- default:
- gdnativeKind = kind
- typeFormat := fmt.Sprintf("VariantType%s", strings.ReplaceAll(kind, "gdnative.", ""))
- _, ok := VariantTypeLookupMap[typeFormat]
- if ok {
- kind = fmt.Sprintf("gdnative.%s", typeFormat)
- break
- }
- kind = "gdnative.VariantTypeObject"
- }
- alias, exported := extractExportedAndAliasFromDoc(field.Doc)
- tmpProperties := []*registryProperty{}
- for _, name := range field.Names {
- if !name.IsExported() && !exported {
- continue
- }
- tmpProperties = append(tmpProperties, ®istryProperty{
- name: name.String(),
- alias: alias,
- kind: kind,
- gdnativeKind: gdnativeKind,
- })
- }
- if field.Tag != nil {
- // check if this tag is the ignore tag
- if field.Tag.Value == "`-`" || field.Tag.Value == "`_`" || field.Tag.Value == "`omit`" {
- continue
- }
- // create a fake reflect.StructTag to lookup our keys
- fakeTag := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", ""))
- rset, rsetOk := fakeTag.Lookup("rset_type")
- usage, usageOk := fakeTag.Lookup("usage")
- hint, hintOk := fakeTag.Lookup("hint")
- hintString, hintStringOk := fakeTag.Lookup("hint_string")
- if !hintStringOk {
- hintString = ""
- }
- if !hintOk {
- hint = "None"
- }
- if !rsetOk {
- rset = "Disabled"
- }
- if !usageOk {
- usage = "Default"
- }
- for i := range tmpProperties {
- mustSetPropertyTagHint(tmpProperties[i], hint)
- mustSetPropertyTagRset(tmpProperties[i], rset)
- mustSetPropertyTagUsage(tmpProperties[i], usage)
- tmpProperties[i].hintString = hintString
- }
- properties = append(properties, tmpProperties...)
- }
- }
- }
- }
- }
- return properties
- }
- func mustSetPropertyTagRset(property *registryProperty, value string) {
- rpcMode := fmt.Sprintf("MethodRpcMode%s", strings.Title(value))
- _, ok := MethodRpcModeLookupMap[rpcMode]
- if !ok {
- valid := []string{}
- for key := range MethodRpcModeLookupMap {
- valid = append(valid, strings.ToLower(key[13:]))
- }
- fmt.Printf(
- "on property %s: unknown rset_type %s, it must be one of:\n\t%s\n",
- property.name, value, strings.Join(valid, "\n\t"),
- )
- os.Exit(1)
- }
- property.rset = strings.Title(value)
- }
- func mustSetPropertyTagHint(property *registryProperty, value string) {
- hint := fmt.Sprintf("PropertyHint%s", strings.Title(value))
- _, ok := PropertyHintLookupMap[hint]
- if !ok {
- valid := []string{}
- for key := range PropertyHintLookupMap {
- valid = append(valid, strings.ToLower(key[12:]))
- }
- fmt.Printf("on property %s: unknown hint %s, it must be one of %s", property.name, value, strings.Join(valid, ", "))
- os.Exit(1)
- }
- property.hint = strings.Title(value)
- }
- func mustSetPropertyTagUsage(property *registryProperty, value string) {
- usage := fmt.Sprintf("PropertyUsage%s", strings.Title(value))
- _, ok := PropertyUsageFlagsLookupMap[usage]
- if !ok {
- valid := []string{}
- for key := range PropertyUsageFlagsLookupMap {
- valid = append(valid, strings.ToLower(key[13:]))
- }
- fmt.Printf("on property %s: unknown usage %s, it must be one of %s", property.name, value, strings.Join(valid, ", "))
- os.Exit(1)
- }
- property.usage = strings.Title(value)
- }
- func extractExportedAndAliasFromDoc(doc *ast.CommentGroup) (string, bool) {
- var alias string
- var exported bool
- if doc != nil {
- for _, line := range doc.List {
- docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
- if strings.HasPrefix(docstring, godotExport) {
- exported = true
- if strings.Contains(docstring, " as ") {
- alias = strings.TrimSpace(strings.Split(strings.Split(docstring, " as ")[1], " ")[0])
- }
- break
- }
- }
- }
- return alias, exported
- }
- func parseDefault(expr ast.Expr, def string) string {
- kind := def
- switch t := expr.(type) {
- case *ast.Ident:
- kind = t.Name
- case *ast.StarExpr:
- kind = parseStarExpr(t)
- case *ast.ParenExpr:
- kind = parseParenExpr(t)
- case *ast.ArrayType:
- kind = parseArray(t)
- case *ast.MapType:
- kind = parseMap(t)
- case *ast.SelectorExpr:
- kind = fmt.Sprintf("%s.%s", parseDefault(t.X, def), t.Sel.String())
- }
- return kind
- }
- func parseStarExpr(expr *ast.StarExpr) string {
- kind := "*%s"
- return fmt.Sprintf(kind, parseDefault(expr.X, "gdnatve.Pointer"))
- }
- func parseParenExpr(field *ast.ParenExpr) string {
- return parseDefault(field.X, "gdnative.Pointer")
- }
- func parseArray(field *ast.ArrayType) string {
- result := "ArrayType[%s]"
- return fmt.Sprintf(result, parseDefault(field.Elt, "ArrayType[]"))
- }
- func parseMap(field *ast.MapType) string {
- result := "MapType[%s]%s"
- key := parseDefault(field.Key, "gdnative.Pointer")
- value := parseDefault(field.Value, "gdnative.Pointer")
- return fmt.Sprintf(result, key, value)
- }
- func parseKeyValueExpr(expr *ast.KeyValueExpr) (string, string) { //nolint:unused
- var value string
- key := expr.Key.(*ast.Ident).Name
- switch t := expr.Value.(type) {
- case *ast.BasicLit:
- value = t.Value
- case *ast.CompositeLit:
- switch t2 := t.Type.(type) {
- case *ast.KeyValueExpr:
- _, value = parseKeyValueExpr(t2)
- default:
- value = parseDefault(t2, "gdnative.Pointer")
- }
- }
- return key, value
- }
- func parseSignalArgs(composite *ast.CompositeLit) string {
- buffer := []byte{}
- buf := bytes.NewBuffer(buffer)
- fileSet := token.NewFileSet()
- printer.Fprint(buf, fileSet, composite)
- return buf.String()
- }
|