godot/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs
Ignacio Roldán Etcheverry 17b2838f39 C#: Cleanup Variant marshaling code in source/bindings generators
This change aims to reduce the number of places that need to be changed
when adding or editing a Godot type to the bindings.

Since the addition of `Variant.From<T>/As<T>` and
`VariantUtils.CreateFrom<T>/ConvertTo<T>`, we can now replace a lot of
the previous code in the bindings generator and the source generators
that specify these conversions for each type manually.

The only exceptions are the generic Godot collections (`Array<T>` and
`Dictionary<TKey, TValue>`) which still use the old version, as that
one cannot be matched by our new conversion methods (limitation in the
language with generics, forcing us to use delegate pointers).

The cleanup applies to:

- Bindings generator:
  - `TypeInterface.cs_variant_to_managed`
  - `TypeInterface.cs_managed_to_variant`
- Source generators:
  - `MarshalUtils.AppendNativeVariantToManagedExpr`
  - `MarshalUtils.AppendManagedToNativeVariantExpr`
  - `MarshalUtils.AppendVariantToManagedExpr`
  - `MarshalUtils.AppendManagedToVariantExpr`
2022-12-02 14:47:12 +01:00

287 lines
10 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptSerializationGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
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 static void VisitGodotScriptClass(
GeneratorExecutionContext context,
MarshalUtils.TypeCache typeCache,
INamedTypeSymbol symbol
)
{
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedNameOmitGlobal() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null;
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().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>()
.Where(s => !s.IsIndexer);
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(PropertyName.")
.Append(propertyName)
.Append(", ")
.AppendManagedToVariantExpr(string.Concat("this.", propertyName),
property.PropertySymbol.Type, property.Type)
.Append(");\n");
}
// Save fields
foreach (var field in godotClassFields)
{
string fieldName = field.FieldSymbol.Name;
source.Append(" info.AddProperty(PropertyName.")
.Append(fieldName)
.Append(", ")
.AppendManagedToVariantExpr(string.Concat("this.", fieldName),
field.FieldSymbol.Type, field.Type)
.Append(");\n");
}
// Save signal events
foreach (var signalDelegate in godotSignalDelegates)
{
string signalName = signalDelegate.Name;
source.Append(" info.AddSignalEventDelegate(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(PropertyName.")
.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(PropertyName.")
.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.FullQualifiedNameIncludeGlobal();
source.Append(" if (info.TryGetSignalEventDelegate<")
.Append(signalDelegateQualifiedName)
.Append(">(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));
}
}
}