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; } } }
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)); }
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)); }