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; } } }
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() + "_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)); }
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)); }
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)); }