Beispiel #1
0
        public void Execute(GeneratorExecutionContext context)
        {
            if (context.AreGodotSourceGeneratorsDisabled())
            {
                return;
            }

            INamedTypeSymbol[] godotClasses = context
                                              .Compilation.SyntaxTrees
                                              .SelectMany(tree =>
                                                          tree.GetRoot().DescendantNodes()
                                                          .OfType <ClassDeclarationSyntax>()
                                                          .SelectGodotScriptClasses(context.Compilation)
                                                          // Report and skip non-partial classes
                                                          .Where(x =>
            {
                if (x.cds.IsPartial())
                {
                    if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
                    {
                        Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial !);
                        return(false);
                    }

                    return(true);
                }

                Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
                return(false);
            })
                                                          .Select(x => x.symbol)
                                                          )
                                              .Distinct <INamedTypeSymbol>(SymbolEqualityComparer.Default)
                                              .ToArray();

            if (godotClasses.Length > 0)
            {
                var typeCache = new MarshalUtils.TypeCache(context.Compilation);

                foreach (var godotClass in godotClasses)
                {
                    VisitGodotScriptClass(context, typeCache, godotClass);
                }
            }
        }
        private void AnalyzeNode(SyntaxNodeAnalysisContext context)
        {
            var typeArgListSyntax = (TypeArgumentListSyntax)context.Node;

            // Method invocation or variable declaration that contained the type arguments
            var parentSyntax = context.Node.Parent;

            Debug.Assert(parentSyntax != null);

            var sm = context.SemanticModel;

            var typeCache = new MarshalUtils.TypeCache(context.Compilation);

            for (int i = 0; i < typeArgListSyntax.Arguments.Count; i++)
            {
                var typeSyntax = typeArgListSyntax.Arguments[i];
                var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol;
                Debug.Assert(typeSymbol != null);

                var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol;

                if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i))
                {
                    return;
                }

                if (typeSymbol is ITypeParameterSymbol typeParamSymbol)
                {
                    if (!typeParamSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false))
                    {
                        Common.ReportGenericTypeParameterMustBeVariantAnnotated(context, typeSyntax, typeSymbol);
                    }
                    continue;
                }

                var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(typeSymbol, typeCache);

                if (marshalType == null)
                {
                    Common.ReportGenericTypeArgumentMustBeVariant(context, typeSyntax, typeSymbol);
                    continue;
                }
            }
        }
Beispiel #3
0
        private static void VisitGodotScriptClass(
            GeneratorExecutionContext context,
            MarshalUtils.TypeCache typeCache,
            INamedTypeSymbol symbol
            )
        {
            INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
            string           classNs         = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
                                               namespaceSymbol.FullQualifiedName() :
                                               string.Empty;
            bool hasNamespace = classNs.Length != 0;

            bool isInnerClass = symbol.ContainingType != null;

            string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()
                                + "_ScriptPropertyDefVal_Generated";

            var source = new StringBuilder();

            source.Append("using Godot;\n");
            source.Append("using Godot.NativeInterop;\n");
            source.Append("\n");

            if (hasNamespace)
            {
                source.Append("namespace ");
                source.Append(classNs);
                source.Append(" {\n\n");
            }

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("partial ");
                    source.Append(containingType.GetDeclarationKeyword());
                    source.Append(" ");
                    source.Append(containingType.NameWithTypeParameters());
                    source.Append("\n{\n");

                    containingType = containingType.ContainingType;
                }
            }

            source.Append("partial class ");
            source.Append(symbol.NameWithTypeParameters());
            source.Append("\n{\n");

            var exportedMembers = new List <ExportedPropertyMetadata>();

            var members = symbol.GetMembers();

            var exportedProperties = members
                                     .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
                                     .Cast <IPropertySymbol>()
                                     .Where(s => s.GetAttributes()
                                            .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false))
                                     .ToArray();

            var exportedFields = members
                                 .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
                                 .Cast <IFieldSymbol>()
                                 .Where(s => s.GetAttributes()
                                        .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false))
                                 .ToArray();

            foreach (var property in exportedProperties)
            {
                if (property.IsStatic)
                {
                    Common.ReportExportedMemberIsStatic(context, property);
                    continue;
                }

                // TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload.
                // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable.
                if (property.IsWriteOnly)
                {
                    Common.ReportExportedMemberIsWriteOnly(context, property);
                    continue;
                }

                if (property.IsReadOnly)
                {
                    Common.ReportExportedMemberIsReadOnly(context, property);
                    continue;
                }


                var propertyType = property.Type;
                var marshalType  = MarshalUtils.ConvertManagedTypeToMarshalType(propertyType, typeCache);

                if (marshalType == null)
                {
                    Common.ReportExportedMemberTypeNotSupported(context, property);
                    continue;
                }

                // TODO: Detect default value from simple property getters (currently we only detect from initializers)

                EqualsValueClauseSyntax?initializer = property.DeclaringSyntaxReferences
                                                      .Select(r => r.GetSyntax() as PropertyDeclarationSyntax)
                                                      .Select(s => s?.Initializer ?? null)
                                                      .FirstOrDefault();

                string?value = initializer?.Value.ToString();

                exportedMembers.Add(new ExportedPropertyMetadata(
                                        property.Name, marshalType.Value, propertyType, value));
            }

            foreach (var field in exportedFields)
            {
                if (field.IsStatic)
                {
                    Common.ReportExportedMemberIsStatic(context, field);
                    continue;
                }

                // TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload.
                // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable.
                if (field.IsReadOnly)
                {
                    Common.ReportExportedMemberIsReadOnly(context, field);
                    continue;
                }

                var fieldType   = field.Type;
                var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(fieldType, typeCache);

                if (marshalType == null)
                {
                    Common.ReportExportedMemberTypeNotSupported(context, field);
                    continue;
                }

                EqualsValueClauseSyntax?initializer = field.DeclaringSyntaxReferences
                                                      .Select(r => r.GetSyntax())
                                                      .OfType <VariableDeclaratorSyntax>()
                                                      .Select(s => s.Initializer)
                                                      .FirstOrDefault(i => i != null);

                string?value = initializer?.Value.ToString();

                exportedMembers.Add(new ExportedPropertyMetadata(
                                        field.Name, marshalType.Value, fieldType, value));
            }

            // Generate GetGodotExportedProperties

            if (exportedMembers.Count > 0)
            {
                source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");

                string dictionaryType = "System.Collections.Generic.Dictionary<StringName, object>";

                source.Append("#if TOOLS\n");
                source.Append("    internal new static ");
                source.Append(dictionaryType);
                source.Append(" GetGodotPropertyDefaultValues()\n    {\n");

                source.Append("        var values = new ");
                source.Append(dictionaryType);
                source.Append("(");
                source.Append(exportedMembers.Count);
                source.Append(");\n");

                foreach (var exportedMember in exportedMembers)
                {
                    string defaultValueLocalName = string.Concat("__", exportedMember.Name, "_default_value");

                    source.Append("        ");
                    source.Append(exportedMember.TypeSymbol.FullQualifiedName());
                    source.Append(" ");
                    source.Append(defaultValueLocalName);
                    source.Append(" = ");
                    source.Append(exportedMember.Value ?? "default");
                    source.Append(";\n");
                    source.Append("        values.Add(GodotInternal.PropName_");
                    source.Append(exportedMember.Name);
                    source.Append(", ");
                    source.Append(defaultValueLocalName);
                    source.Append(");\n");
                }

                source.Append("        return values;\n");
                source.Append("    }\n");
                source.Append("#endif\n");

                source.Append("#pragma warning restore CS0109\n");
            }

            source.Append("}\n"); // partial class

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("}\n"); // outer class

                    containingType = containingType.ContainingType;
                }
            }

            if (hasNamespace)
            {
                source.Append("\n}\n");
            }

            context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
        }
Beispiel #4
0
        private static void VisitGodotScriptClass(
            GeneratorExecutionContext context,
            MarshalUtils.TypeCache typeCache,
            INamedTypeSymbol symbol
            )
        {
            INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
            string           classNs         = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
                                               namespaceSymbol.FullQualifiedName() :
                                               string.Empty;
            bool hasNamespace = classNs.Length != 0;

            bool isInnerClass = symbol.ContainingType != null;

            string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()
                                + "_ScriptSerialization_Generated";

            var source = new StringBuilder();

            source.Append("using Godot;\n");
            source.Append("using Godot.NativeInterop;\n");
            source.Append("\n");

            if (hasNamespace)
            {
                source.Append("namespace ");
                source.Append(classNs);
                source.Append(" {\n\n");
            }

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("partial ");
                    source.Append(containingType.GetDeclarationKeyword());
                    source.Append(" ");
                    source.Append(containingType.NameWithTypeParameters());
                    source.Append("\n{\n");

                    containingType = containingType.ContainingType;
                }
            }

            source.Append("partial class ");
            source.Append(symbol.NameWithTypeParameters());
            source.Append("\n{\n");

            var members = symbol.GetMembers();

            var propertySymbols = members
                                  .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
                                  .Cast <IPropertySymbol>();

            var fieldSymbols = members
                               .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
                               .Cast <IFieldSymbol>();

            var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
            var godotClassFields     = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray();

            var signalDelegateSymbols = members
                                        .Where(s => s.Kind == SymbolKind.NamedType)
                                        .Cast <INamedTypeSymbol>()
                                        .Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate)
                                        .Where(s => s.GetAttributes()
                                               .Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false));

            List <GodotSignalDelegateData> godotSignalDelegates = new();

            foreach (var signalDelegateSymbol in signalDelegateSymbols)
            {
                if (!signalDelegateSymbol.Name.EndsWith(ScriptSignalsGenerator.SignalDelegateSuffix))
                {
                    continue;
                }

                string signalName = signalDelegateSymbol.Name;
                signalName = signalName.Substring(0,
                                                  signalName.Length - ScriptSignalsGenerator.SignalDelegateSuffix.Length);

                var invokeMethodData = signalDelegateSymbol
                                       .DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache);

                if (invokeMethodData == null)
                {
                    continue;
                }

                godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value));
            }

            source.Append(
                "    protected override void SaveGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n    {\n");
            source.Append("        base.SaveGodotObjectData(info);\n");

            // Save properties

            foreach (var property in godotClassProperties)
            {
                string propertyName = property.PropertySymbol.Name;

                source.Append("        info.AddProperty(GodotInternal.PropName_")
                .Append(propertyName)
                .Append(", ")
                .AppendManagedToVariantExpr(string.Concat("this.", propertyName), property.Type)
                .Append(");\n");
            }

            // Save fields

            foreach (var field in godotClassFields)
            {
                string fieldName = field.FieldSymbol.Name;

                source.Append("        info.AddProperty(GodotInternal.PropName_")
                .Append(fieldName)
                .Append(", ")
                .AppendManagedToVariantExpr(string.Concat("this.", fieldName), field.Type)
                .Append(");\n");
            }

            // Save signal events

            foreach (var signalDelegate in godotSignalDelegates)
            {
                string signalName = signalDelegate.Name;

                source.Append("        info.AddSignalEventDelegate(GodotInternal.SignalName_")
                .Append(signalName)
                .Append(", this.backing_")
                .Append(signalName)
                .Append(");\n");
            }

            source.Append("    }\n");

            source.Append(
                "    protected override void RestoreGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n    {\n");
            source.Append("        base.RestoreGodotObjectData(info);\n");

            // Restore properties

            foreach (var property in godotClassProperties)
            {
                string propertyName = property.PropertySymbol.Name;

                source.Append("        if (info.TryGetProperty(GodotInternal.PropName_")
                .Append(propertyName)
                .Append(", out var _value_")
                .Append(propertyName)
                .Append("))\n")
                .Append("            this.")
                .Append(propertyName)
                .Append(" = ")
                .AppendVariantToManagedExpr(string.Concat("_value_", propertyName),
                                            property.PropertySymbol.Type, property.Type)
                .Append(";\n");
            }

            // Restore fields

            foreach (var field in godotClassFields)
            {
                string fieldName = field.FieldSymbol.Name;

                source.Append("        if (info.TryGetProperty(GodotInternal.PropName_")
                .Append(fieldName)
                .Append(", out var _value_")
                .Append(fieldName)
                .Append("))\n")
                .Append("            this.")
                .Append(fieldName)
                .Append(" = ")
                .AppendVariantToManagedExpr(string.Concat("_value_", fieldName),
                                            field.FieldSymbol.Type, field.Type)
                .Append(";\n");
            }

            // Restore signal events

            foreach (var signalDelegate in godotSignalDelegates)
            {
                string signalName = signalDelegate.Name;
                string signalDelegateQualifiedName = signalDelegate.DelegateSymbol.FullQualifiedName();

                source.Append("        if (info.TryGetSignalEventDelegate<")
                .Append(signalDelegateQualifiedName)
                .Append(">(GodotInternal.SignalName_")
                .Append(signalName)
                .Append(", out var _value_")
                .Append(signalName)
                .Append("))\n")
                .Append("            this.backing_")
                .Append(signalName)
                .Append(" = _value_")
                .Append(signalName)
                .Append(";\n");
            }

            source.Append("    }\n");

            source.Append("}\n"); // partial class

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("}\n"); // outer class

                    containingType = containingType.ContainingType;
                }
            }

            if (hasNamespace)
            {
                source.Append("\n}\n");
            }

            context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
        }
Beispiel #5
0
        private static void VisitGodotScriptClass(
            GeneratorExecutionContext context,
            MarshalUtils.TypeCache typeCache,
            INamedTypeSymbol symbol
            )
        {
            INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
            string           classNs         = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
                                               namespaceSymbol.FullQualifiedName() :
                                               string.Empty;
            bool hasNamespace = classNs.Length != 0;

            bool isInnerClass = symbol.ContainingType != null;

            string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()
                                + "_ScriptSignals_Generated";

            var source = new StringBuilder();

            source.Append("using Godot;\n");
            source.Append("using Godot.NativeInterop;\n");
            source.Append("\n");

            if (hasNamespace)
            {
                source.Append("namespace ");
                source.Append(classNs);
                source.Append(" {\n\n");
            }

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("partial ");
                    source.Append(containingType.GetDeclarationKeyword());
                    source.Append(" ");
                    source.Append(containingType.NameWithTypeParameters());
                    source.Append("\n{\n");

                    containingType = containingType.ContainingType;
                }
            }

            source.Append("partial class ");
            source.Append(symbol.NameWithTypeParameters());
            source.Append("\n{\n");

            var members = symbol.GetMembers();

            var signalDelegateSymbols = members
                                        .Where(s => s.Kind == SymbolKind.NamedType)
                                        .Cast <INamedTypeSymbol>()
                                        .Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate)
                                        .Where(s => s.GetAttributes()
                                               .Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false));

            List <GodotSignalDelegateData> godotSignalDelegates = new();

            foreach (var signalDelegateSymbol in signalDelegateSymbols)
            {
                if (!signalDelegateSymbol.Name.EndsWith(SignalDelegateSuffix))
                {
                    Common.ReportSignalDelegateMissingSuffix(context, signalDelegateSymbol);
                    continue;
                }

                string signalName = signalDelegateSymbol.Name;
                signalName = signalName.Substring(0, signalName.Length - SignalDelegateSuffix.Length);

                var invokeMethodData = signalDelegateSymbol
                                       .DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache);

                if (invokeMethodData == null)
                {
                    if (signalDelegateSymbol.DelegateInvokeMethod is IMethodSymbol methodSymbol)
                    {
                        foreach (var parameter in methodSymbol.Parameters)
                        {
                            if (parameter.RefKind != RefKind.None)
                            {
                                Common.ReportSignalParameterTypeNotSupported(context, parameter);
                                continue;
                            }

                            var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(parameter.Type, typeCache);

                            if (marshalType == null)
                            {
                                Common.ReportSignalParameterTypeNotSupported(context, parameter);
                            }
                        }

                        if (!methodSymbol.ReturnsVoid)
                        {
                            Common.ReportSignalDelegateSignatureMustReturnVoid(context, signalDelegateSymbol);
                        }
                    }
                    continue;
                }

                godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value));
            }

            source.Append("    private partial class GodotInternal {\n");

            // Generate cached StringNames for methods and properties, for fast lookup

            foreach (var signalDelegate in godotSignalDelegates)
            {
                string signalName = signalDelegate.Name;
                source.Append("        public static readonly StringName SignalName_");
                source.Append(signalName);
                source.Append(" = \"");
                source.Append(signalName);
                source.Append("\";\n");
            }

            source.Append("    }\n"); // class GodotInternal

            // Generate GetGodotSignalList

            if (godotSignalDelegates.Count > 0)
            {
                source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");

                const string listType = "System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>";

                source.Append("    internal new static ")
                .Append(listType)
                .Append(" GetGodotSignalList()\n    {\n");

                source.Append("        var signals = new ")
                .Append(listType)
                .Append("(")
                .Append(godotSignalDelegates.Count)
                .Append(");\n");

                foreach (var signalDelegateData in godotSignalDelegates)
                {
                    var methodInfo = DetermineMethodInfo(signalDelegateData);
                    AppendMethodInfo(source, methodInfo);
                }

                source.Append("        return signals;\n");
                source.Append("    }\n");

                source.Append("#pragma warning restore CS0109\n");
            }

            // Generate signal event

            foreach (var signalDelegate in godotSignalDelegates)
            {
                string signalName = signalDelegate.Name;

                // TODO: Hide backing event from code-completion and debugger
                // The reason we have a backing field is to hide the invoke method from the event,
                // as it doesn't emit the signal, only the event delegates. This can confuse users.
                // Maybe we should directly connect the delegates, as we do with native signals?
                source.Append("    private ")
                .Append(signalDelegate.DelegateSymbol.FullQualifiedName())
                .Append(" backing_")
                .Append(signalName)
                .Append(";\n");

                source.Append("    public event ")
                .Append(signalDelegate.DelegateSymbol.FullQualifiedName())
                .Append(" ")
                .Append(signalName)
                .Append(" {\n")
                .Append("        add => backing_")
                .Append(signalName)
                .Append(" += value;\n")
                .Append("        remove => backing_")
                .Append(signalName)
                .Append(" -= value;\n")
                .Append("}\n");
            }

            // Generate RaiseGodotClassSignalCallbacks

            if (godotSignalDelegates.Count > 0)
            {
                source.Append(
                    "    protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, ");
                source.Append("NativeVariantPtrArgs args, int argCount)\n    {\n");

                foreach (var signal in godotSignalDelegates)
                {
                    GenerateSignalEventInvoker(signal, source);
                }

                source.Append("        base.RaiseGodotClassSignalCallbacks(signal, args, argCount);\n");

                source.Append("    }\n");
            }

            source.Append("}\n"); // partial class

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("}\n"); // outer class

                    containingType = containingType.ContainingType;
                }
            }

            if (hasNamespace)
            {
                source.Append("\n}\n");
            }

            context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
        }
        private static void VisitGodotScriptClass(
            GeneratorExecutionContext context,
            MarshalUtils.TypeCache typeCache,
            INamedTypeSymbol symbol
            )
        {
            INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
            string           classNs         = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
                                               namespaceSymbol.FullQualifiedName() :
                                               string.Empty;
            bool hasNamespace = classNs.Length != 0;

            bool isInnerClass = symbol.ContainingType != null;

            string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()
                                + "_ScriptProperties_Generated";

            var source = new StringBuilder();

            source.Append("using Godot;\n");
            source.Append("using Godot.NativeInterop;\n");
            source.Append("\n");

            if (hasNamespace)
            {
                source.Append("namespace ");
                source.Append(classNs);
                source.Append(" {\n\n");
            }

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("partial ");
                    source.Append(containingType.GetDeclarationKeyword());
                    source.Append(" ");
                    source.Append(containingType.NameWithTypeParameters());
                    source.Append("\n{\n");

                    containingType = containingType.ContainingType;
                }
            }

            source.Append("partial class ");
            source.Append(symbol.NameWithTypeParameters());
            source.Append("\n{\n");

            var members = symbol.GetMembers();

            var propertySymbols = members
                                  .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
                                  .Cast <IPropertySymbol>();

            var fieldSymbols = members
                               .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
                               .Cast <IFieldSymbol>();

            var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
            var godotClassFields     = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray();

            source.Append("    private partial class GodotInternal {\n");

            // Generate cached StringNames for methods and properties, for fast lookup

            foreach (var property in godotClassProperties)
            {
                string propertyName = property.PropertySymbol.Name;
                source.Append("        public static readonly StringName PropName_");
                source.Append(propertyName);
                source.Append(" = \"");
                source.Append(propertyName);
                source.Append("\";\n");
            }

            foreach (var field in godotClassFields)
            {
                string fieldName = field.FieldSymbol.Name;
                source.Append("        public static readonly StringName PropName_");
                source.Append(fieldName);
                source.Append(" = \"");
                source.Append(fieldName);
                source.Append("\";\n");
            }

            source.Append("    }\n"); // class GodotInternal

            if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
            {
                bool isFirstEntry;

                // Generate SetGodotClassPropertyValue

                bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) &&
                                                godotClassProperties.All(pi => pi.PropertySymbol.IsReadOnly);

                if (!allPropertiesAreReadOnly)
                {
                    source.Append("    protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
                    source.Append("in godot_variant value)\n    {\n");

                    isFirstEntry = true;
                    foreach (var property in godotClassProperties)
                    {
                        if (property.PropertySymbol.IsReadOnly)
                        {
                            continue;
                        }

                        GeneratePropertySetter(property.PropertySymbol.Name,
                                               property.PropertySymbol.Type, property.Type, source, isFirstEntry);
                        isFirstEntry = false;
                    }

                    foreach (var field in godotClassFields)
                    {
                        if (field.FieldSymbol.IsReadOnly)
                        {
                            continue;
                        }

                        GeneratePropertySetter(field.FieldSymbol.Name,
                                               field.FieldSymbol.Type, field.Type, source, isFirstEntry);
                        isFirstEntry = false;
                    }

                    source.Append("        return base.SetGodotClassPropertyValue(name, value);\n");

                    source.Append("    }\n");
                }

                // Generate GetGodotClassPropertyValue

                source.Append("    protected override bool GetGodotClassPropertyValue(in godot_string_name name, ");
                source.Append("out godot_variant value)\n    {\n");

                isFirstEntry = true;
                foreach (var property in godotClassProperties)
                {
                    GeneratePropertyGetter(property.PropertySymbol.Name,
                                           property.Type, source, isFirstEntry);
                    isFirstEntry = false;
                }

                foreach (var field in godotClassFields)
                {
                    GeneratePropertyGetter(field.FieldSymbol.Name,
                                           field.Type, source, isFirstEntry);
                    isFirstEntry = false;
                }

                source.Append("        return base.GetGodotClassPropertyValue(name, out value);\n");

                source.Append("    }\n");

                // Generate GetGodotPropertyList

                source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");

                string dictionaryType = "System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>";

                source.Append("    internal new static ")
                .Append(dictionaryType)
                .Append(" GetGodotPropertyList()\n    {\n");

                source.Append("        var properties = new ")
                .Append(dictionaryType)
                .Append("();\n");

                // To retain the definition order (and display categories correctly), we want to
                //  iterate over fields and properties at the same time, sorted by line number.
                var godotClassPropertiesAndFields = Enumerable.Empty <GodotPropertyOrFieldData>()
                                                    .Concat(godotClassProperties.Select(propertyData => new GodotPropertyOrFieldData(propertyData)))
                                                    .Concat(godotClassFields.Select(fieldData => new GodotPropertyOrFieldData(fieldData)))
                                                    .OrderBy(data => data.Symbol.Locations[0].Path())
                                                    .ThenBy(data => data.Symbol.Locations[0].StartLine());

                foreach (var member in godotClassPropertiesAndFields)
                {
                    foreach (var groupingInfo in DetermineGroupingPropertyInfo(member.Symbol))
                    {
                        AppendGroupingPropertyInfo(source, groupingInfo);
                    }

                    var propertyInfo = DeterminePropertyInfo(context, typeCache,
                                                             member.Symbol, member.Type);

                    if (propertyInfo == null)
                    {
                        continue;
                    }

                    AppendPropertyInfo(source, propertyInfo.Value);
                }

                source.Append("        return properties;\n");
                source.Append("    }\n");

                source.Append("#pragma warning restore CS0109\n");
            }

            source.Append("}\n"); // partial class

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("}\n"); // outer class

                    containingType = containingType.ContainingType;
                }
            }

            if (hasNamespace)
            {
                source.Append("\n}\n");
            }

            context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
        }
        private static bool TryGetMemberExportHint(
            MarshalUtils.TypeCache typeCache,
            ITypeSymbol type, AttributeData exportAttr,
            VariantType variantType, bool isTypeArgument,
            out PropertyHint hint, out string?hintString
            )
        {
            hint       = PropertyHint.None;
            hintString = null;

            if (variantType == VariantType.Nil)
            {
                return(true); // Variant, no export hint
            }
            if (variantType == VariantType.Int &&
                type.IsValueType && type.TypeKind == TypeKind.Enum)
            {
                bool hasFlagsAttr = type.GetAttributes()
                                    .Any(a => a.AttributeClass?.IsSystemFlagsAttribute() ?? false);

                hint = hasFlagsAttr ? PropertyHint.Flags : PropertyHint.Enum;

                var members = type.GetMembers();

                var enumFields = members
                                 .Where(s => s.Kind == SymbolKind.Field && s.IsStatic &&
                                        s.DeclaredAccessibility == Accessibility.Public &&
                                        !s.IsImplicitlyDeclared)
                                 .Cast <IFieldSymbol>().ToArray();

                var hintStringBuilder         = new StringBuilder();
                var nameOnlyHintStringBuilder = new StringBuilder();

                // True: enum Foo { Bar, Baz, Qux }
                // True: enum Foo { Bar = 0, Baz = 1, Qux = 2 }
                // False: enum Foo { Bar = 0, Baz = 7, Qux = 5 }
                bool usesDefaultValues = true;

                for (int i = 0; i < enumFields.Length; i++)
                {
                    var enumField = enumFields[i];

                    if (i > 0)
                    {
                        hintStringBuilder.Append(",");
                        nameOnlyHintStringBuilder.Append(",");
                    }

                    string enumFieldName = enumField.Name;
                    hintStringBuilder.Append(enumFieldName);
                    nameOnlyHintStringBuilder.Append(enumFieldName);

                    long val = enumField.ConstantValue switch
                    {
                        sbyte v => v,
                        short v => v,
                        int v => v,
                        long v => v,
                        byte v => v,
                        ushort v => v,
                        uint v => v,
                        ulong v => (long)v,
                        _ => 0
                    };

                    uint expectedVal = (uint)(hint == PropertyHint.Flags ? 1 << i : i);
                    if (val != expectedVal)
                    {
                        usesDefaultValues = false;
                    }

                    hintStringBuilder.Append(":");
                    hintStringBuilder.Append(val);
                }

                hintString = !usesDefaultValues?
                             hintStringBuilder.ToString() :
                                 // If we use the format NAME:VAL, that's what the editor displays.
                                 // That's annoying if the user is not using custom values for the enum constants.
                                 // This may not be needed in the future if the editor is changed to not display values.
                                 nameOnlyHintStringBuilder.ToString();

                return(true);
            }

            if (variantType == VariantType.Object && type is INamedTypeSymbol memberNamedType)
            {
                if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Resource"))
                {
                    string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName() !;

                    hint       = PropertyHint.ResourceType;
                    hintString = nativeTypeName;

                    return(true);
                }

                if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Node"))
                {
                    string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName() !;

                    hint       = PropertyHint.NodeType;
                    hintString = nativeTypeName;

                    return(true);
                }
            }
        private static PropertyInfo?DeterminePropertyInfo(
            GeneratorExecutionContext context,
            MarshalUtils.TypeCache typeCache,
            ISymbol memberSymbol,
            MarshalType marshalType
            )
        {
            var exportAttr = memberSymbol.GetAttributes()
                             .FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false);

            var propertySymbol = memberSymbol as IPropertySymbol;
            var fieldSymbol    = memberSymbol as IFieldSymbol;

            if (exportAttr != null && propertySymbol != null)
            {
                if (propertySymbol.GetMethod == null)
                {
                    // This should never happen, as we filtered WriteOnly properties, but just in case.
                    Common.ReportExportedMemberIsWriteOnly(context, propertySymbol);
                    return(null);
                }

                if (propertySymbol.SetMethod == null)
                {
                    // This should never happen, as we filtered ReadOnly properties, but just in case.
                    Common.ReportExportedMemberIsReadOnly(context, propertySymbol);
                    return(null);
                }
            }

            var memberType = propertySymbol?.Type ?? fieldSymbol !.Type;

            var    memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType) !.Value;
            string memberName        = memberSymbol.Name;

            if (exportAttr == null)
            {
                return(new PropertyInfo(memberVariantType, memberName, PropertyHint.None,
                                        hintString: null, PropertyUsageFlags.ScriptVariable, exported: false));
            }

            if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType,
                                        isTypeArgument: false, out var hint, out var hintString))
            {
                var constructorArguments = exportAttr.ConstructorArguments;

                if (constructorArguments.Length > 0)
                {
                    var hintValue = exportAttr.ConstructorArguments[0].Value;

                    hint = hintValue switch
                    {
                        null => PropertyHint.None,
                        int intValue => (PropertyHint)intValue,
                        _ => (PropertyHint)(long)hintValue
                    };

                    hintString = constructorArguments.Length > 1 ?
                                 exportAttr.ConstructorArguments[1].Value?.ToString() :
                                 null;
                }
                else
                {
                    hint = PropertyHint.None;
                }
            }

            var propUsage = PropertyUsageFlags.Default | PropertyUsageFlags.ScriptVariable;

            if (memberVariantType == VariantType.Nil)
            {
                propUsage |= PropertyUsageFlags.NilIsVariant;
            }

            return(new PropertyInfo(memberVariantType, memberName,
                                    hint, hintString, propUsage, exported: true));
        }
Beispiel #9
0
        private static void VisitGodotScriptClass(
            GeneratorExecutionContext context,
            MarshalUtils.TypeCache typeCache,
            INamedTypeSymbol symbol
            )
        {
            INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
            string           classNs         = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
                                               namespaceSymbol.FullQualifiedName() :
                                               string.Empty;
            bool hasNamespace = classNs.Length != 0;

            bool isInnerClass = symbol.ContainingType != null;

            string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()
                                + "_ScriptMethods_Generated";

            var source = new StringBuilder();

            source.Append("using Godot;\n");
            source.Append("using Godot.NativeInterop;\n");
            source.Append("\n");

            if (hasNamespace)
            {
                source.Append("namespace ");
                source.Append(classNs);
                source.Append(" {\n\n");
            }

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("partial ");
                    source.Append(containingType.GetDeclarationKeyword());
                    source.Append(" ");
                    source.Append(containingType.NameWithTypeParameters());
                    source.Append("\n{\n");

                    containingType = containingType.ContainingType;
                }
            }

            source.Append("partial class ");
            source.Append(symbol.NameWithTypeParameters());
            source.Append("\n{\n");

            var members = symbol.GetMembers();

            var methodSymbols = members
                                .Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared)
                                .Cast <IMethodSymbol>()
                                .Where(m => m.MethodKind == MethodKind.Ordinary);

            var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache)
                                    .Distinct(new MethodOverloadEqualityComparer())
                                    .ToArray();

            source.Append("    private partial class GodotInternal {\n");

            // Generate cached StringNames for methods and properties, for fast lookup

            var distinctMethodNames = godotClassMethods
                                      .Select(m => m.Method.Name)
                                      .Distinct()
                                      .ToArray();

            foreach (string methodName in distinctMethodNames)
            {
                source.Append("        public static readonly StringName MethodName_");
                source.Append(methodName);
                source.Append(" = \"");
                source.Append(methodName);
                source.Append("\";\n");
            }

            source.Append("    }\n"); // class GodotInternal

            // Generate GetGodotMethodList

            if (godotClassMethods.Length > 0)
            {
                source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");

                const string listType = "System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>";

                source.Append("    internal new static ")
                .Append(listType)
                .Append(" GetGodotMethodList()\n    {\n");

                source.Append("        var methods = new ")
                .Append(listType)
                .Append("(")
                .Append(godotClassMethods.Length)
                .Append(");\n");

                foreach (var method in godotClassMethods)
                {
                    var methodInfo = DetermineMethodInfo(method);
                    AppendMethodInfo(source, methodInfo);
                }

                source.Append("        return methods;\n");
                source.Append("    }\n");

                source.Append("#pragma warning restore CS0109\n");
            }

            // Generate InvokeGodotClassMethod

            if (godotClassMethods.Length > 0)
            {
                source.Append("    protected override bool InvokeGodotClassMethod(in godot_string_name method, ");
                source.Append("NativeVariantPtrArgs args, int argCount, out godot_variant ret)\n    {\n");

                foreach (var method in godotClassMethods)
                {
                    GenerateMethodInvoker(method, source);
                }

                source.Append("        return base.InvokeGodotClassMethod(method, args, argCount, out ret);\n");

                source.Append("    }\n");
            }

            // Generate HasGodotClassMethod

            if (distinctMethodNames.Length > 0)
            {
                source.Append("    protected override bool HasGodotClassMethod(in godot_string_name method)\n    {\n");

                bool isFirstEntry = true;
                foreach (string methodName in distinctMethodNames)
                {
                    GenerateHasMethodEntry(methodName, source, isFirstEntry);
                    isFirstEntry = false;
                }

                source.Append("        return base.HasGodotClassMethod(method);\n");

                source.Append("    }\n");
            }

            source.Append("}\n"); // partial class

            if (isInnerClass)
            {
                var containingType = symbol.ContainingType;

                while (containingType != null)
                {
                    source.Append("}\n"); // outer class

                    containingType = containingType.ContainingType;
                }
            }

            if (hasNamespace)
            {
                source.Append("\n}\n");
            }

            context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
        }