using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace Godot.SourceGenerators { [Generator] public class ScriptMemberInvokerGenerator : 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 ) { 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() + "_ScriptMemberInvoker_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() .Where(m => m.MethodKind == MethodKind.Ordinary); var propertySymbols = members .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) .Cast(); var fieldSymbols = members .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) .Cast(); var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache).ToArray(); 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 // TODO: Move the generation of these cached StringNames to its own generator 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"); } 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.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"); } // 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.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 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(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); } private static void GenerateMethodInvoker( GodotMethodData 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(" var callRet = "); else source.Append(" "); source.Append(methodName); source.Append("("); for (int i = 0; i < method.ParamTypes.Length; i++) { if (i != 0) source.Append(", "); source.AppendVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"), method.ParamTypeSymbols[i], method.ParamTypes[i]); } source.Append(");\n"); if (method.RetType != null) { source.Append(" ret = "); source.AppendManagedToVariantExpr("callRet", method.RetType.Value); source.Append(";\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, ITypeSymbol propertyTypeSymbol, MarshalType propertyMarshalType, StringBuilder source, bool isFirstEntry ) { source.Append(" "); if (!isFirstEntry) source.Append("else "); source.Append("if (name == GodotInternal.PropName_") .Append(propertyMemberName) .Append(") {\n") .Append(" ") .Append(propertyMemberName) .Append(" = ") .AppendVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType) .Append(";\n") .Append(" return true;\n") .Append(" }\n"); } private static void GeneratePropertyGetter( string propertyMemberName, MarshalType propertyMarshalType, StringBuilder source, bool isFirstEntry ) { source.Append(" "); if (!isFirstEntry) source.Append("else "); source.Append("if (name == GodotInternal.PropName_") .Append(propertyMemberName) .Append(") {\n") .Append(" value = ") .AppendManagedToVariantExpr(propertyMemberName, propertyMarshalType) .Append(";\n") .Append(" return true;\n") .Append(" }\n"); } private static void GenerateHasMethodEntry( GodotMethodData 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) { } } }