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 #2
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 #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()
                                + "_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));
        }
            static bool GetStringArrayEnumHint(VariantType elementVariantType,
                                               AttributeData exportAttr, out string?hintString)
            {
                var constructorArguments = exportAttr.ConstructorArguments;

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

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

                    if (presetHint == PropertyHint.Enum)
                    {
                        string?presetHintString = constructorArguments.Length > 1 ?
                                                  exportAttr.ConstructorArguments[1].Value?.ToString() :
                                                  null;

                        hintString = (int)elementVariantType + "/" + (int)PropertyHint.Enum + ":";

                        if (presetHintString != null)
                        {
                            hintString += presetHintString;
                        }

                        return(true);
                    }
                }

                hintString = null;
                return(false);
            }

            if (!isTypeArgument && variantType == VariantType.Array)
            {
                var elementType = MarshalUtils.GetArrayElementType(type);

                if (elementType == null)
                {
                    return(false); // Non-generic Array, so there's no hint to add
                }
                var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache) !.Value;
                var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType) !.Value;

                bool isPresetHint = false;

                if (elementVariantType == VariantType.String)
                {
                    isPresetHint = GetStringArrayEnumHint(elementVariantType, exportAttr, out hintString);
                }

                if (!isPresetHint)
                {
                    bool hintRes = TryGetMemberExportHint(typeCache, elementType,
                                                          exportAttr, elementVariantType, isTypeArgument: true,
                                                          out var elementHint, out var elementHintString);

                    // Format: type/hint:hint_string
                    if (hintRes)
                    {
                        hintString = (int)elementVariantType + "/" + (int)elementHint + ":";

                        if (elementHintString != null)
                        {
                            hintString += elementHintString;
                        }
                    }
                    else
                    {
                        hintString = (int)elementVariantType + "/" + (int)PropertyHint.None + ":";
                    }
                }

                hint = PropertyHint.TypeString;

                return(hintString != null);
            }

            if (!isTypeArgument && variantType == VariantType.PackedStringArray)
            {
                if (GetStringArrayEnumHint(VariantType.String, exportAttr, out hintString))
                {
                    hint = PropertyHint.TypeString;
                    return(true);
                }
            }

            if (!isTypeArgument && variantType == VariantType.Dictionary)
            {
                // TODO: Dictionaries are not supported in the inspector
                return(false);
            }

            return(false);
        }
        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));
        }