You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-11-12 13:20:55 +00:00
C#: Add source generator for signals as events
Changed the signal declaration signal to: ``` // The following generates a MySignal event [Signal] public delegate void MySignalEventHandler(int param); ```
This commit is contained in:
@@ -166,5 +166,54 @@ namespace Godot.SourceGenerators
|
||||
location,
|
||||
location?.SourceTree?.FilePath));
|
||||
}
|
||||
|
||||
public static void ReportSignalDelegateMissingSuffix(
|
||||
GeneratorExecutionContext context,
|
||||
INamedTypeSymbol delegateSymbol)
|
||||
{
|
||||
var locations = delegateSymbol.Locations;
|
||||
var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault();
|
||||
|
||||
string message = "The name of the delegate must end with 'EventHandler': " +
|
||||
delegateSymbol.ToDisplayString() +
|
||||
$". Did you mean '{delegateSymbol.Name}EventHandler'?";
|
||||
|
||||
string description = $"{message}. Rename the delegate accordingly or remove the '[Signal]' attribute.";
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
new DiagnosticDescriptor(id: "GODOT-G0201",
|
||||
title: message,
|
||||
messageFormat: message,
|
||||
category: "Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description),
|
||||
location,
|
||||
location?.SourceTree?.FilePath));
|
||||
}
|
||||
|
||||
public static void ReportSignalDelegateSignatureNotSupported(
|
||||
GeneratorExecutionContext context,
|
||||
INamedTypeSymbol delegateSymbol)
|
||||
{
|
||||
var locations = delegateSymbol.Locations;
|
||||
var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault();
|
||||
|
||||
string message = "The delegate signature of the signal " +
|
||||
$"is not supported: '{delegateSymbol.ToDisplayString()}'";
|
||||
|
||||
string description = $"{message}. Use supported types only or remove the '[Signal]' attribute.";
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
new DiagnosticDescriptor(id: "GODOT-G0202",
|
||||
title: message,
|
||||
messageFormat: message,
|
||||
category: "Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description),
|
||||
location,
|
||||
location?.SourceTree?.FilePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,12 +174,49 @@ namespace Godot.SourceGenerators
|
||||
public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol)
|
||||
=> symbol.ToString() == GodotClasses.ExportAttr;
|
||||
|
||||
public static bool IsGodotSignalAttribute(this INamedTypeSymbol symbol)
|
||||
=> symbol.ToString() == GodotClasses.SignalAttr;
|
||||
|
||||
public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol)
|
||||
=> symbol.ToString() == GodotClasses.GodotClassNameAttr;
|
||||
|
||||
public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol)
|
||||
=> symbol.ToString() == GodotClasses.SystemFlagsAttr;
|
||||
|
||||
public static GodotMethodData? HasGodotCompatibleSignature(
|
||||
this IMethodSymbol method,
|
||||
MarshalUtils.TypeCache typeCache
|
||||
)
|
||||
{
|
||||
if (method.IsGenericMethod)
|
||||
return null;
|
||||
|
||||
var retSymbol = method.ReturnType;
|
||||
var retType = method.ReturnsVoid ?
|
||||
null :
|
||||
MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache);
|
||||
|
||||
if (retType == null && !method.ReturnsVoid)
|
||||
return null;
|
||||
|
||||
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.ConvertManagedTypeToMarshalType(p.Type, typeCache))
|
||||
// Discard parameter types that couldn't be determined (null entries)
|
||||
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
|
||||
|
||||
// If any parameter type was incompatible, it was discarded so the length won't match
|
||||
if (parameters.Length > paramTypes.Length)
|
||||
return null; // Ignore incompatible method
|
||||
|
||||
return new GodotMethodData(method, paramTypes, parameters
|
||||
.Select(p => p.Type).ToImmutableArray(), retType, retSymbol);
|
||||
}
|
||||
|
||||
public static IEnumerable<GodotMethodData> WhereHasGodotCompatibleSignature(
|
||||
this IEnumerable<IMethodSymbol> methods,
|
||||
MarshalUtils.TypeCache typeCache
|
||||
@@ -187,33 +224,10 @@ namespace Godot.SourceGenerators
|
||||
{
|
||||
foreach (var method in methods)
|
||||
{
|
||||
if (method.IsGenericMethod)
|
||||
continue;
|
||||
var methodData = HasGodotCompatibleSignature(method, typeCache);
|
||||
|
||||
var retSymbol = method.ReturnType;
|
||||
var retType = method.ReturnsVoid ?
|
||||
null :
|
||||
MarshalUtils.ConvertManagedTypeToMarshalType(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.ConvertManagedTypeToMarshalType(p.Type, typeCache))
|
||||
// Discard parameter types that couldn't be determined (null entries)
|
||||
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
|
||||
|
||||
// If any parameter type was incompatible, it was discarded so the length won't match
|
||||
if (parameters.Length > paramTypes.Length)
|
||||
continue;
|
||||
|
||||
yield return new GodotMethodData(method, paramTypes, parameters
|
||||
.Select(p => p.Type).ToImmutableArray(), retType, retSymbol);
|
||||
if (methodData != null)
|
||||
yield return methodData.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Godot.SourceGenerators
|
||||
public const string Object = "Godot.Object";
|
||||
public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute";
|
||||
public const string ExportAttr = "Godot.ExportAttribute";
|
||||
public const string SignalAttr = "Godot.SignalAttribute";
|
||||
public const string GodotClassNameAttr = "Godot.GodotClassName";
|
||||
public const string SystemFlagsAttr = "System.FlagsAttribute";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
|
||||
namespace Godot.SourceGenerators
|
||||
{
|
||||
// TODO: May need to think about compatibility here. Could Godot change these values between minor versions?
|
||||
|
||||
internal enum VariantType
|
||||
{
|
||||
Nil = 0,
|
||||
@@ -131,4 +133,16 @@ namespace Godot.SourceGenerators
|
||||
DefaultIntl = 38,
|
||||
NoEditor = 2
|
||||
}
|
||||
|
||||
public enum MethodFlags
|
||||
{
|
||||
Normal = 1,
|
||||
Editor = 2,
|
||||
Const = 4,
|
||||
Virtual = 8,
|
||||
Vararg = 16,
|
||||
Static = 32,
|
||||
ObjectCore = 64,
|
||||
Default = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,20 @@ namespace Godot.SourceGenerators
|
||||
public ITypeSymbol? RetSymbol { get; }
|
||||
}
|
||||
|
||||
public struct GodotSignalDelegateData
|
||||
{
|
||||
public GodotSignalDelegateData(string name, INamedTypeSymbol delegateSymbol, GodotMethodData invokeMethodData)
|
||||
{
|
||||
Name = name;
|
||||
DelegateSymbol = delegateSymbol;
|
||||
InvokeMethodData = invokeMethodData;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public INamedTypeSymbol DelegateSymbol { get; }
|
||||
public GodotMethodData InvokeMethodData { get; }
|
||||
}
|
||||
|
||||
public struct GodotPropertyData
|
||||
{
|
||||
public GodotPropertyData(IPropertySymbol propertySymbol, MarshalType type)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Godot.SourceGenerators
|
||||
{
|
||||
internal struct MethodInfo
|
||||
{
|
||||
public MethodInfo(string name, PropertyInfo returnVal, MethodFlags flags,
|
||||
List<PropertyInfo>? arguments,
|
||||
List<string?>? defaultArguments)
|
||||
{
|
||||
Name = name;
|
||||
ReturnVal = returnVal;
|
||||
Flags = flags;
|
||||
Arguments = arguments;
|
||||
DefaultArguments = defaultArguments;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public PropertyInfo ReturnVal { get; }
|
||||
public MethodFlags Flags { get; }
|
||||
public List<PropertyInfo>? Arguments { get; }
|
||||
public List<string?>? DefaultArguments { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Godot.SourceGenerators
|
||||
{
|
||||
internal struct PropertyInfo
|
||||
{
|
||||
public PropertyInfo(VariantType type, string name, PropertyHint hint,
|
||||
string? hintString, PropertyUsageFlags usage, bool exported)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
Hint = hint;
|
||||
HintString = hintString;
|
||||
Usage = usage;
|
||||
Exported = exported;
|
||||
}
|
||||
|
||||
public VariantType Type { get; }
|
||||
public string Name { get; }
|
||||
public PropertyHint Hint { get; }
|
||||
public string? HintString { get; }
|
||||
public PropertyUsageFlags Usage { get; }
|
||||
public bool Exported { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -122,6 +123,33 @@ namespace Godot.SourceGenerators
|
||||
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(" private partial class GodotInternal {\n");
|
||||
|
||||
// Generate cached StringNames for methods and properties, for fast lookup
|
||||
@@ -157,6 +185,42 @@ namespace Godot.SourceGenerators
|
||||
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");
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Generate Set/GetGodotClassPropertyValue
|
||||
|
||||
if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
|
||||
@@ -224,24 +288,6 @@ namespace Godot.SourceGenerators
|
||||
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)
|
||||
@@ -314,6 +360,39 @@ namespace Godot.SourceGenerators
|
||||
source.Append(" }\n");
|
||||
}
|
||||
|
||||
private static void GenerateSignalEventInvoker(
|
||||
GodotSignalDelegateData signal,
|
||||
StringBuilder source
|
||||
)
|
||||
{
|
||||
string signalName = signal.Name;
|
||||
var invokeMethodData = signal.InvokeMethodData;
|
||||
|
||||
source.Append(" if (signal == GodotInternal.SignalName_");
|
||||
source.Append(signalName);
|
||||
source.Append(" && argCount == ");
|
||||
source.Append(invokeMethodData.ParamTypes.Length);
|
||||
source.Append(") {\n");
|
||||
source.Append(" backing_");
|
||||
source.Append(signalName);
|
||||
source.Append("?.Invoke(");
|
||||
|
||||
for (int i = 0; i < invokeMethodData.ParamTypes.Length; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
source.Append(", ");
|
||||
|
||||
source.AppendVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"),
|
||||
invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]);
|
||||
}
|
||||
|
||||
source.Append(");\n");
|
||||
|
||||
source.Append(" return;\n");
|
||||
|
||||
source.Append(" }\n");
|
||||
}
|
||||
|
||||
private static void GeneratePropertySetter(
|
||||
string propertyMemberName,
|
||||
ITypeSymbol propertyTypeSymbol,
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace Godot.SourceGenerators
|
||||
|
||||
source.Append(" }\n"); // class GodotInternal
|
||||
|
||||
// Generate GetGodotPropertiesMetadata
|
||||
// Generate GetGodotPropertyList
|
||||
|
||||
if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
|
||||
{
|
||||
@@ -156,7 +156,7 @@ namespace Godot.SourceGenerators
|
||||
|
||||
source.Append(" internal new static ")
|
||||
.Append(dictionaryType)
|
||||
.Append(" GetGodotPropertiesMetadata()\n {\n");
|
||||
.Append(" GetGodotPropertyList()\n {\n");
|
||||
|
||||
source.Append(" var properties = new ")
|
||||
.Append(dictionaryType)
|
||||
@@ -164,7 +164,7 @@ namespace Godot.SourceGenerators
|
||||
|
||||
foreach (var property in godotClassProperties)
|
||||
{
|
||||
var propertyInfo = GetPropertyMetadata(context, typeCache,
|
||||
var propertyInfo = DeterminePropertyInfo(context, typeCache,
|
||||
property.PropertySymbol, property.Type);
|
||||
|
||||
if (propertyInfo == null)
|
||||
@@ -175,7 +175,7 @@ namespace Godot.SourceGenerators
|
||||
|
||||
foreach (var field in godotClassFields)
|
||||
{
|
||||
var propertyInfo = GetPropertyMetadata(context, typeCache,
|
||||
var propertyInfo = DeterminePropertyInfo(context, typeCache,
|
||||
field.FieldSymbol, field.Type);
|
||||
|
||||
if (propertyInfo == null)
|
||||
@@ -229,28 +229,7 @@ namespace Godot.SourceGenerators
|
||||
.Append("));\n");
|
||||
}
|
||||
|
||||
private struct PropertyInfo
|
||||
{
|
||||
public PropertyInfo(VariantType type, string name, PropertyHint hint,
|
||||
string? hintString, PropertyUsageFlags usage, bool exported)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
Hint = hint;
|
||||
HintString = hintString;
|
||||
Usage = usage;
|
||||
Exported = exported;
|
||||
}
|
||||
|
||||
public VariantType Type { get; }
|
||||
public string Name { get; }
|
||||
public PropertyHint Hint { get; }
|
||||
public string? HintString { get; }
|
||||
public PropertyUsageFlags Usage { get; }
|
||||
public bool Exported { get; }
|
||||
}
|
||||
|
||||
private static PropertyInfo? GetPropertyMetadata(
|
||||
private static PropertyInfo? DeterminePropertyInfo(
|
||||
GeneratorExecutionContext context,
|
||||
MarshalUtils.TypeCache typeCache,
|
||||
ISymbol memberSymbol,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -120,10 +121,39 @@ namespace Godot.SourceGenerators
|
||||
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;
|
||||
@@ -135,6 +165,8 @@ namespace Godot.SourceGenerators
|
||||
.Append(");\n");
|
||||
}
|
||||
|
||||
// Save fields
|
||||
|
||||
foreach (var field in godotClassFields)
|
||||
{
|
||||
string fieldName = field.FieldSymbol.Name;
|
||||
@@ -146,12 +178,27 @@ namespace Godot.SourceGenerators
|
||||
.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;
|
||||
@@ -171,6 +218,8 @@ namespace Godot.SourceGenerators
|
||||
.Append(";\n");
|
||||
}
|
||||
|
||||
// Restore fields
|
||||
|
||||
foreach (var field in godotClassFields)
|
||||
{
|
||||
string fieldName = field.FieldSymbol.Name;
|
||||
@@ -190,6 +239,27 @@ namespace Godot.SourceGenerators
|
||||
.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
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
// TODO:
|
||||
// Determine a proper way to emit the signal.
|
||||
// 'Emit(nameof(TheEvent))' creates a StringName everytime and has the overhead of string marshaling.
|
||||
// I haven't decided on the best option yet. Some possibilities:
|
||||
// - Expose the generated StringName fields to the user, for use with 'Emit(...)'.
|
||||
// - Generate a 'EmitSignalName' method for each event signal.
|
||||
|
||||
namespace Godot.SourceGenerators
|
||||
{
|
||||
[Generator]
|
||||
public class ScriptSignalsGenerator : 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);
|
||||
|
||||
foreach (var godotClass in godotClasses)
|
||||
{
|
||||
VisitGodotScriptClass(context, typeCache, godotClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static string SignalDelegateSuffix = "EventHandler";
|
||||
|
||||
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");
|
||||
|
||||
// TODO:
|
||||
// The delegate name already needs to end with 'Signal' to avoid collision with the event name.
|
||||
// Requiring SignalAttribute is redundant. Should we remove it to make declaration shorter?
|
||||
|
||||
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)
|
||||
{
|
||||
// TODO: Better error for incompatible signature. We should indicate incompatible argument types, as we do with exported properties.
|
||||
Common.ReportSignalDelegateSignatureNotSupported(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");
|
||||
}
|
||||
|
||||
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 AppendMethodInfo(StringBuilder source, MethodInfo methodInfo)
|
||||
{
|
||||
source.Append(" signals.Add(new(name: GodotInternal.SignalName_")
|
||||
.Append(methodInfo.Name)
|
||||
.Append(", returnVal: ");
|
||||
|
||||
AppendPropertyInfo(source, methodInfo.ReturnVal);
|
||||
|
||||
source.Append(", flags: (Godot.MethodFlags)")
|
||||
.Append((int)methodInfo.Flags)
|
||||
.Append(", arguments: ");
|
||||
|
||||
if (methodInfo.Arguments is { Count: > 0 })
|
||||
{
|
||||
source.Append("new() { ");
|
||||
|
||||
foreach (var param in methodInfo.Arguments)
|
||||
{
|
||||
AppendPropertyInfo(source, param);
|
||||
|
||||
// C# allows colon after the last element
|
||||
source.Append(", ");
|
||||
}
|
||||
|
||||
source.Append(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
source.Append("null");
|
||||
}
|
||||
|
||||
source.Append(", defaultArguments: null));\n");
|
||||
}
|
||||
|
||||
private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
|
||||
{
|
||||
source.Append("new(type: (Godot.Variant.Type)")
|
||||
.Append((int)propertyInfo.Type)
|
||||
.Append(", name: \"")
|
||||
.Append(propertyInfo.Name)
|
||||
.Append("\", hint: (Godot.PropertyHint)")
|
||||
.Append((int)propertyInfo.Hint)
|
||||
.Append(", hintString: \"")
|
||||
.Append(propertyInfo.HintString)
|
||||
.Append("\", usage: (Godot.PropertyUsageFlags)")
|
||||
.Append((int)propertyInfo.Usage)
|
||||
.Append(", exported: ")
|
||||
.Append(propertyInfo.Exported ? "true" : "false")
|
||||
.Append(")");
|
||||
}
|
||||
|
||||
private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDelegateData)
|
||||
{
|
||||
var invokeMethodData = signalDelegateData.InvokeMethodData;
|
||||
|
||||
PropertyInfo returnVal;
|
||||
|
||||
if (invokeMethodData.RetType != null)
|
||||
{
|
||||
returnVal = DeterminePropertyInfo(invokeMethodData.RetType.Value, name: string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None,
|
||||
hintString: null, PropertyUsageFlags.Default, exported: false);
|
||||
}
|
||||
|
||||
int paramCount = invokeMethodData.ParamTypes.Length;
|
||||
|
||||
List<PropertyInfo>? arguments;
|
||||
|
||||
if (paramCount > 0)
|
||||
{
|
||||
arguments = new(capacity: paramCount);
|
||||
|
||||
for (int i = 0; i < paramCount; i++)
|
||||
{
|
||||
arguments.Add(DeterminePropertyInfo(invokeMethodData.ParamTypes[i],
|
||||
name: invokeMethodData.Method.Parameters[i].Name));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments = null;
|
||||
}
|
||||
|
||||
return new MethodInfo(signalDelegateData.Name, returnVal, MethodFlags.Default, arguments,
|
||||
defaultArguments: null);
|
||||
}
|
||||
|
||||
private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, string name)
|
||||
{
|
||||
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
|
||||
|
||||
var propUsage = PropertyUsageFlags.Default;
|
||||
|
||||
if (memberVariantType == VariantType.Nil)
|
||||
propUsage |= PropertyUsageFlags.NilIsVariant;
|
||||
|
||||
return new PropertyInfo(memberVariantType, name,
|
||||
PropertyHint.None, string.Empty, propUsage, exported: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user