virtualx-engine/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs
Ignacio Roldán Etcheverry 92503ae8db C#: Add source generator for properties and exports default values
The editor no longer needs to create temporary instances to get the
default values. The initializer values of the exported properties are
still evaluated at runtime. For example, in the following example,
`GetInitialValue()` will be called when first looks for default values:

```
[Export] int MyValue = GetInitialValue();
```

Exporting fields with a non-supported type now results in a compiler
error rather than a runtime error when the script is used.
2022-08-22 03:36:51 +02:00

397 lines
14 KiB
C#

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<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);
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();
// TODO: Static static marshaling (no reflection, no runtime type checks)
var methodSymbols = members
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared)
.Cast<IMethodSymbol>()
.Where(m => m.MethodKind == MethodKind.Ordinary);
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 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.FullQualifiedName(), source, isFirstEntry);
isFirstEntry = false;
}
foreach (var field in godotClassFields)
{
if (field.FieldSymbol.IsReadOnly)
continue;
GeneratePropertySetter(field.FieldSymbol.Name,
field.FieldSymbol.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.PropertySymbol.Name, source, isFirstEntry);
isFirstEntry = false;
}
foreach (var field in godotClassFields)
{
GeneratePropertyGetter(field.FieldSymbol.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(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(" 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(
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)
{
}
}
}