using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace Godot.SourceGenerators { [Generator] public class ScriptBoilerplateGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { if (context.AreGodotSourceGeneratorsDisabled()) return; INamedTypeSymbol[] godotClasses = context .Compilation.SyntaxTrees .SelectMany(tree => tree.GetRoot().DescendantNodes() .OfType() .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(SymbolEqualityComparer.Default) .ToArray(); if (godotClasses.Length > 0) { var typeCache = new MarshalUtils.TypeCache(context); foreach (var godotClass in godotClasses) { VisitGodotScriptClass(context, typeCache, godotClass); } } } private static void VisitGodotScriptClass( GeneratorExecutionContext context, MarshalUtils.TypeCache typeCache, INamedTypeSymbol symbol ) { string className = symbol.Name; 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 uniqueName = hasNamespace ? classNs + "." + className + "_ScriptBoilerplate_Generated" : className + "_ScriptBoilerplate_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.Name); source.Append("\n{\n"); containingType = containingType.ContainingType; } } source.Append("partial class "); source.Append(className); source.Append("\n{\n"); var members = symbol.GetMembers(); // TODO: Static static marshaling (no reflection, no runtime type checks) var methodSymbols = members .Where(s => s.Kind == SymbolKind.Method) .Cast() .Where(m => m.MethodKind == MethodKind.Ordinary && !m.IsImplicitlyDeclared); var propertySymbols = members .Where(s => s.Kind == SymbolKind.Property) .Cast(); var fieldSymbols = members .Where(s => s.Kind == SymbolKind.Field) .Cast() .Where(p => !p.IsImplicitlyDeclared); var godotClassMethods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray(); var godotClassProperties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray(); var godotClassFields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray(); source.Append(" private class GodotInternal {\n"); // Generate cached StringNames for methods and properties, for fast lookup foreach (var method in godotClassMethods) { string methodName = method.Method.Name; source.Append(" public static readonly StringName MethodName_"); source.Append(methodName); source.Append(" = \""); source.Append(methodName); source.Append("\";\n"); } foreach (var property in godotClassProperties) { string propertyName = property.Property.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.Field.Name; source.Append(" public static readonly StringName PropName_"); source.Append(fieldName); source.Append(" = \""); source.Append(fieldName); source.Append("\";\n"); } source.Append(" }\n"); // class GodotInternal // 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 Set/GetGodotClassPropertyValue if (godotClassProperties.Length > 0 || godotClassFields.Length > 0) { bool isFirstEntry; // Setters bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.Field.IsReadOnly) && godotClassProperties.All(pi => pi.Property.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.Property.IsReadOnly) continue; GeneratePropertySetter(property.Property.Name, property.Property.Type.FullQualifiedName(), source, isFirstEntry); isFirstEntry = false; } foreach (var field in godotClassFields) { if (field.Field.IsReadOnly) continue; GeneratePropertySetter(field.Field.Name, field.Field.Type.FullQualifiedName(), source, isFirstEntry); isFirstEntry = false; } source.Append(" return base.SetGodotClassPropertyValue(name, value);\n"); source.Append(" }\n"); } // Getters 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.Property.Name, source, isFirstEntry); isFirstEntry = false; } foreach (var field in godotClassFields) { GeneratePropertyGetter(field.Field.Name, source, isFirstEntry); isFirstEntry = false; } source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n"); source.Append(" }\n"); } // Generate HasGodotClassMethod if (godotClassMethods.Length > 0) { source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n"); bool isFirstEntry = true; foreach (var method in godotClassMethods) { GenerateHasMethodEntry(method, 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(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8)); } private static void GenerateMethodInvoker( GodotMethodInfo method, StringBuilder source ) { string methodName = method.Method.Name; source.Append(" if (method == GodotInternal.MethodName_"); source.Append(methodName); source.Append(" && argCount == "); source.Append(method.ParamTypes.Length); source.Append(") {\n"); if (method.RetType != null) source.Append(" object retBoxed = "); else source.Append(" "); source.Append(methodName); source.Append("("); for (int i = 0; i < method.ParamTypes.Length; i++) { if (i != 0) source.Append(", "); // TODO: static marshaling (no reflection, no runtime type checks) string paramTypeQualifiedName = method.ParamTypeSymbols[i].FullQualifiedName(); source.Append("("); source.Append(paramTypeQualifiedName); source.Append(")Marshaling.ConvertVariantToManagedObjectOfType(args["); source.Append(i); source.Append("], typeof("); source.Append(paramTypeQualifiedName); source.Append("))"); } source.Append(");\n"); if (method.RetType != null) { // TODO: static marshaling (no reflection, no runtime type checks) source.Append(" ret = Marshaling.ConvertManagedObjectToVariant(retBoxed);\n"); source.Append(" return true;\n"); } else { source.Append(" ret = default;\n"); source.Append(" return true;\n"); } source.Append(" }\n"); } private static void GeneratePropertySetter( string propertyMemberName, string propertyTypeQualifiedName, StringBuilder source, bool isFirstEntry ) { source.Append(" "); if (!isFirstEntry) source.Append("else "); source.Append("if (name == GodotInternal.PropName_"); source.Append(propertyMemberName); source.Append(") {\n"); source.Append(" "); source.Append(propertyMemberName); source.Append(" = "); // TODO: static marshaling (no reflection, no runtime type checks) source.Append("("); source.Append(propertyTypeQualifiedName); source.Append(")Marshaling.ConvertVariantToManagedObjectOfType(value, typeof("); source.Append(propertyTypeQualifiedName); source.Append("));\n"); source.Append(" return true;\n"); source.Append(" }\n"); } private static void GeneratePropertyGetter( string propertyMemberName, StringBuilder source, bool isFirstEntry ) { source.Append(" "); if (!isFirstEntry) source.Append("else "); source.Append("if (name == GodotInternal.PropName_"); source.Append(propertyMemberName); source.Append(") {\n"); // TODO: static marshaling (no reflection, no runtime type checks) source.Append(" value = Marshaling.ConvertManagedObjectToVariant("); source.Append(propertyMemberName); source.Append(");\n"); source.Append(" return true;\n"); source.Append(" }\n"); } private static void GenerateHasMethodEntry( GodotMethodInfo method, StringBuilder source, bool isFirstEntry ) { string methodName = method.Method.Name; source.Append(" "); if (!isFirstEntry) source.Append("else "); source.Append("if (method == GodotInternal.MethodName_"); source.Append(methodName); source.Append(") {\n return true;\n }\n"); } public void Initialize(GeneratorInitializationContext context) { } private struct GodotMethodInfo { public GodotMethodInfo(IMethodSymbol method, ImmutableArray paramTypes, ImmutableArray paramTypeSymbols, MarshalType? retType) { Method = method; ParamTypes = paramTypes; ParamTypeSymbols = paramTypeSymbols; RetType = retType; } public IMethodSymbol Method { get; } public ImmutableArray ParamTypes { get; } public ImmutableArray ParamTypeSymbols { get; } public MarshalType? RetType { get; } } private struct GodotPropertyInfo { public GodotPropertyInfo(IPropertySymbol property, MarshalType type) { Property = property; Type = type; } public IPropertySymbol Property { get; } public MarshalType Type { get; } } private struct GodotFieldInfo { public GodotFieldInfo(IFieldSymbol field, MarshalType type) { Field = field; Type = type; } public IFieldSymbol Field { get; } public MarshalType Type { get; } } private static IEnumerable WhereHasCompatibleGodotType( IEnumerable methods, MarshalUtils.TypeCache typeCache ) { foreach (var method in methods) { if (method.IsGenericMethod) continue; var retType = method.ReturnsVoid ? null : MarshalUtils.ConvertManagedTypeToVariantType(method.ReturnType, typeCache); if (retType == null && !method.ReturnsVoid) continue; var parameters = method.Parameters; var paramTypes = parameters // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may) .Where(p => p.RefKind == RefKind.None) // Attempt to determine the variant type .Select(p => MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache)) // Discard parameter types that couldn't be determined (null entries) .Where(t => t != null).Cast().ToImmutableArray(); // If any parameter type was incompatible, it was discarded so the length won't match if (parameters.Length > paramTypes.Length) continue; // Ignore incompatible method yield return new GodotMethodInfo(method, paramTypes, parameters .Select(p => p.Type).ToImmutableArray(), retType); } } private static IEnumerable WhereIsCompatibleGodotType( IEnumerable properties, MarshalUtils.TypeCache typeCache ) { foreach (var property in properties) { // Ignore properties without a getter. Godot properties must be readable. if (property.IsWriteOnly) continue; var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(property.Type, typeCache); if (marshalType == null) continue; yield return new GodotPropertyInfo(property, marshalType.Value); } } private static IEnumerable WhereIsCompatibleGodotType( IEnumerable fields, MarshalUtils.TypeCache typeCache ) { foreach (var field in fields) { var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(field.Type, typeCache); if (marshalType == null) continue; yield return new GodotFieldInfo(field, marshalType.Value); } } } }