diff options
author | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-02-27 21:57:30 +0100 |
---|---|---|
committer | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-08-22 03:36:51 +0200 |
commit | 92503ae8dbdf8f0f543dd785b79d3ec13b19092f (patch) | |
tree | 18e276bd75919eb701b625338b58af653821687e /modules/mono/editor | |
parent | 88e367a4066773a6fbfe2ea25dc2e81d2035d791 (diff) |
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.
Diffstat (limited to 'modules/mono/editor')
19 files changed, 1711 insertions, 246 deletions
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs new file mode 100644 index 0000000000..7b106ef63c --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS0169 +#pragma warning disable CS0414 + +namespace Godot.SourceGenerators.Sample +{ + [SuppressMessage("ReSharper", "BuiltInTypeReferenceStyle")] + [SuppressMessage("ReSharper", "RedundantNameQualifier")] + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public partial class ExportedFields : Godot.Object + { + [Export] private Boolean field_Boolean = true; + [Export] private Char field_Char = 'f'; + [Export] private SByte field_SByte = 10; + [Export] private Int16 field_Int16 = 10; + [Export] private Int32 field_Int32 = 10; + [Export] private Int64 field_Int64 = 10; + [Export] private Byte field_Byte = 10; + [Export] private UInt16 field_UInt16 = 10; + [Export] private UInt32 field_UInt32 = 10; + [Export] private UInt64 field_UInt64 = 10; + [Export] private Single field_Single = 10; + [Export] private Double field_Double = 10; + [Export] private String field_String = "foo"; + + // Godot structs + [Export] private Vector2 field_Vector2 = new(10f, 10f); + [Export] private Vector2i field_Vector2i = Vector2i.Up; + [Export] private Rect2 field_Rect2 = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2i field_Rect2i = new(new Vector2i(10, 10), new Vector2i(10, 10)); + [Export] private Transform2D field_Transform2D = Transform2D.Identity; + [Export] private Vector3 field_Vector3 = new(10f, 10f, 10f); + [Export] private Vector3i field_Vector3i = Vector3i.Back; + [Export] private Basis field_Basis = new Basis(Quaternion.Identity); + [Export] private Quaternion field_Quaternion = new Quaternion(Basis.Identity); + [Export] private Transform3D field_Transform3D = Transform3D.Identity; + [Export] private Vector4 field_Vector4 = new(10f, 10f, 10f, 10f); + [Export] private Vector4i field_Vector4i = Vector4i.One; + [Export] private Projection field_Projection = Projection.Identity; + [Export] private AABB field_AABB = new AABB(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color field_Color = Colors.Aquamarine; + [Export] private Plane field_Plane = Plane.PlaneXZ; + [Export] private Callable field_Callable = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private SignalInfo field_SignalInfo = new SignalInfo(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum field_Enum = MyEnum.C; + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum field_FlagsEnum = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] field_ByteArray = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] field_Int32Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] field_Int64Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] field_SingleArray = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] field_DoubleArray = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] field_StringArray = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] field_StringArrayEnum = { "foo", "bar" }; + [Export] private Vector2[] field_Vector2Array = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] field_Vector3Array = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] field_ColorArray = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private Godot.Object[] field_GodotObjectOrDerivedArray = { null }; + [Export] private object[] field_SystemObjectArray = { 0, 1f, 2d, "foo", Vector3i.Up }; + + // Generics + [Export] private Godot.Collections.Dictionary<string, string> field_GodotGenericDictionary = + new Godot.Collections.Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private Godot.Collections.Array<string> field_GodotGenericArray = + new Godot.Collections.Array<string> { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.Dictionary<string, string> field_SystemGenericDictionary = + new System.Collections.Generic.Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.List<string> field_SystemGenericList = + new System.Collections.Generic.List<string> { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IDictionary<string, string> field_GenericIDictionary = + new System.Collections.Generic.Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.ICollection<string> field_GenericICollection = + new System.Collections.Generic.List<string> { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IEnumerable<string> field_GenericIEnumerable = + new System.Collections.Generic.List<string> { "elem1", "elem2", "elem3" }; + + // Variant + [Export] private object field_SystemObject = "foo"; + + // Classes + [Export] private Godot.Object field_GodotObjectOrDerived; + [Export] private Godot.Texture field_GodotResourceTexture; + [Export] private StringName field_StringName = new StringName("foo"); + [Export] private NodePath field_NodePath = new NodePath("foo"); + [Export] private RID field_RID; + + [Export] private Godot.Collections.Dictionary field_GodotDictionary = + new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private Godot.Collections.Array field_GodotArray = + new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IDictionary field_IDictionary = + new System.Collections.Generic.Dictionary<object, object> + { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private System.Collections.ICollection field_ICollection = + new System.Collections.Generic.List<object> { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IEnumerable field_IEnumerable = + new System.Collections.Generic.List<object> { "foo", 10, Vector2.Up, Colors.Chocolate }; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs new file mode 100644 index 0000000000..71025c1d43 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS0169 +#pragma warning disable CS0414 + +namespace Godot.SourceGenerators.Sample +{ + [SuppressMessage("ReSharper", "BuiltInTypeReferenceStyle")] + [SuppressMessage("ReSharper", "RedundantNameQualifier")] + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public partial class ExportedProperties : Godot.Object + { + [Export] private Boolean property_Boolean { get; set; } = true; + [Export] private Char property_Char { get; set; } = 'f'; + [Export] private SByte property_SByte { get; set; } = 10; + [Export] private Int16 property_Int16 { get; set; } = 10; + [Export] private Int32 property_Int32 { get; set; } = 10; + [Export] private Int64 property_Int64 { get; set; } = 10; + [Export] private Byte property_Byte { get; set; } = 10; + [Export] private UInt16 property_UInt16 { get; set; } = 10; + [Export] private UInt32 property_UInt32 { get; set; } = 10; + [Export] private UInt64 property_UInt64 { get; set; } = 10; + [Export] private Single property_Single { get; set; } = 10; + [Export] private Double property_Double { get; set; } = 10; + [Export] private String property_String { get; set; } = "foo"; + + // Godot structs + [Export] private Vector2 property_Vector2 { get; set; } = new(10f, 10f); + [Export] private Vector2i property_Vector2i { get; set; } = Vector2i.Up; + [Export] private Rect2 property_Rect2 { get; set; } = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2i property_Rect2i { get; set; } = new(new Vector2i(10, 10), new Vector2i(10, 10)); + [Export] private Transform2D property_Transform2D { get; set; } = Transform2D.Identity; + [Export] private Vector3 property_Vector3 { get; set; } = new(10f, 10f, 10f); + [Export] private Vector3i property_Vector3i { get; set; } = Vector3i.Back; + [Export] private Basis property_Basis { get; set; } = new Basis(Quaternion.Identity); + [Export] private Quaternion property_Quaternion { get; set; } = new Quaternion(Basis.Identity); + [Export] private Transform3D property_Transform3D { get; set; } = Transform3D.Identity; + [Export] private Vector4 property_Vector4 { get; set; } = new(10f, 10f, 10f, 10f); + [Export] private Vector4i property_Vector4i { get; set; } = Vector4i.One; + [Export] private Projection property_Projection { get; set; } = Projection.Identity; + [Export] private AABB property_AABB { get; set; } = new AABB(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color property_Color { get; set; } = Colors.Aquamarine; + [Export] private Plane property_Plane { get; set; } = Plane.PlaneXZ; + [Export] private Callable property_Callable { get; set; } = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private SignalInfo property_SignalInfo { get; set; } = new SignalInfo(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum property_Enum { get; set; } = MyEnum.C; + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum property_FlagsEnum { get; set; } = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] property_ByteArray { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] property_Int32Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] property_Int64Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] property_SingleArray { get; set; } = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] property_DoubleArray { get; set; } = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] property_StringArray { get; set; } = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] property_StringArrayEnum { get; set; } = { "foo", "bar" }; + [Export] private Vector2[] property_Vector2Array { get; set; } = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] property_Vector3Array { get; set; } = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] property_ColorArray { get; set; } = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private Godot.Object[] property_GodotObjectOrDerivedArray { get; set; } = { null }; + [Export] private object[] property_SystemObjectArray { get; set; } = { 0, 1f, 2d, "foo", Vector3i.Up }; + + // Generics + [Export] private Godot.Collections.Dictionary<string, string> property_GodotGenericDictionary { get; set; } = + new Godot.Collections.Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private Godot.Collections.Array<string> property_GodotGenericArray { get; set; } = + new Godot.Collections.Array<string> { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.Dictionary<string, string> property_SystemGenericDictionary { get; set; } = + new System.Collections.Generic.Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.List<string> property_SystemGenericList { get; set; } = + new System.Collections.Generic.List<string> { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IDictionary<string, string> property_GenericIDictionary { get; set; } = + new System.Collections.Generic.Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.ICollection<string> property_GenericICollection { get; set; } = + new System.Collections.Generic.List<string> { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IEnumerable<string> property_GenericIEnumerable { get; set; } = + new System.Collections.Generic.List<string> { "elem1", "elem2", "elem3" }; + + // Variant + [Export] private object property_SystemObject { get; set; } = "foo"; + + // Classes + [Export] private Godot.Object property_GodotObjectOrDerived { get; set; } + [Export] private Godot.Texture property_GodotResourceTexture { get; set; } + [Export] private StringName property_StringName { get; set; } = new StringName("foo"); + [Export] private NodePath property_NodePath { get; set; } = new NodePath("foo"); + [Export] private RID property_RID { get; set; } + + [Export] private Godot.Collections.Dictionary property_GodotDictionary { get; set; } = + new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private Godot.Collections.Array property_GodotArray { get; set; } = + new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IDictionary property_IDictionary { get; set; } = + new System.Collections.Generic.Dictionary<object, object> + { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private System.Collections.ICollection property_ICollection { get; set; } = + new System.Collections.Generic.List<object> { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IEnumerable property_IEnumerable { get; set; } = + new System.Collections.Generic.List<object> { "foo", 10, Vector2.Up, Colors.Chocolate }; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs index 2ddb8880c2..b21b035b4d 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs @@ -1,16 +1,21 @@ +#pragma warning disable CS0169 + namespace Godot.SourceGenerators.Sample { partial class Generic<T> : Godot.Object { + private int _field; } // Generic again but different generic parameters partial class Generic<T, R> : Godot.Object { + private int _field; } // Generic again but without generic parameters partial class Generic : Godot.Object { + private int _field; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj index c5a29a53f7..a042fb313f 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj @@ -7,6 +7,8 @@ <PropertyGroup> <!-- $(GodotProjectDir) would normally be defined by the Godot.NET.Sdk --> <GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir> + <!-- For compiling GetGodotPropertyDefaultValues. --> + <DefineConstants>$(DefineConstants);TOOLS</DefineConstants> </PropertyGroup> <PropertyGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs index 0c7328284e..bfc8ef2fb5 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs @@ -1,4 +1,4 @@ -using System; +#pragma warning disable CS0169 namespace Godot.SourceGenerators.Sample { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs index fa41c85322..0b8a2777e5 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -60,5 +61,110 @@ namespace Godot.SourceGenerators outerTypeDeclSyntax.GetLocation(), outerTypeDeclSyntax.SyntaxTree.FilePath)); } + + public static void ReportExportedMemberIsStatic( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"Attempted to export static {(isField ? "field" : "property")}: " + + $"'{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Only instance fields and properties can be exported." + + " Remove the 'static' modifier or the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0101", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberTypeNotSupported( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"The type of the exported {(isField ? "field" : "property")} " + + $"is not supported: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Use a supported type or remove the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0102", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsReadOnly( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"The exported {(isField ? "field" : "property")} " + + $"is read-only: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = isField ? + $"{message}. Exported fields cannot be read-only." : + $"{message}. Exported properties must be writable."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0103", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsWriteOnly( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = $"The exported property is write-only: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Exported properties must be readable."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0104", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index 9586e71d02..2179aeea88 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -24,30 +25,55 @@ namespace Godot.SourceGenerators toggle != null && toggle.Equals("true", StringComparison.OrdinalIgnoreCase); - private static bool InheritsFrom(this INamedTypeSymbol? symbol, string baseName) + public static bool InheritsFrom(this INamedTypeSymbol? symbol, string assemblyName, string typeFullName) { - if (symbol == null) - return false; - - while (true) + while (symbol != null) { - if (symbol.ToString() == baseName) + if (symbol.ContainingAssembly.Name == assemblyName && + symbol.ToString() == typeFullName) { return true; } - if (symbol.BaseType != null) - { - symbol = symbol.BaseType; - continue; - } - - break; + symbol = symbol.BaseType; } return false; } + public static INamedTypeSymbol? GetGodotScriptNativeClass(this INamedTypeSymbol classTypeSymbol) + { + var symbol = classTypeSymbol; + + while (symbol != null) + { + if (symbol.ContainingAssembly.Name == "GodotSharp") + return symbol; + + symbol = symbol.BaseType; + } + + return null; + } + + public static string? GetGodotScriptNativeClassName(this INamedTypeSymbol classTypeSymbol) + { + var nativeType = classTypeSymbol.GetGodotScriptNativeClass(); + + if (nativeType == null) + return null; + + var godotClassNameAttr = nativeType.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.IsGodotClassNameAttribute() ?? false); + + string? godotClassName = null; + + if (godotClassNameAttr is { ConstructorArguments: { Length: > 0 } }) + godotClassName = godotClassNameAttr.ConstructorArguments[0].Value?.ToString(); + + return godotClassName ?? nativeType.Name; + } + private static bool IsGodotScriptClass( this ClassDeclarationSyntax cds, Compilation compilation, out INamedTypeSymbol? symbol @@ -58,7 +84,7 @@ namespace Godot.SourceGenerators var classTypeSymbol = sm.GetDeclaredSymbol(cds); if (classTypeSymbol?.BaseType == null - || !classTypeSymbol.BaseType.InheritsFrom(GodotClasses.Object)) + || !classTypeSymbol.BaseType.InheritsFrom("GodotSharp", GodotClasses.Object)) { symbol = null; return false; @@ -129,7 +155,101 @@ namespace Godot.SourceGenerators public static string FullQualifiedName(this ITypeSymbol symbol) => symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal); + public static string NameWithTypeParameters(this INamedTypeSymbol symbol) + { + return symbol.IsGenericType ? + string.Concat(symbol.Name, "<", string.Join(", ", symbol.TypeParameters), ">") : + symbol.Name; + } + public static string FullQualifiedName(this INamespaceSymbol namespaceSymbol) => namespaceSymbol.ToDisplayString(FullyQualifiedFormatOmitGlobal); + + public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedName) + => qualifiedName + // AddSource() doesn't support angle brackets + .Replace("<", "(Of ") + .Replace(">", ")"); + + public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.ExportAttr; + + public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.GodotClassNameAttr; + + public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.SystemFlagsAttr; + + public static IEnumerable<GodotMethodData> WhereHasGodotCompatibleSignature( + this IEnumerable<IMethodSymbol> methods, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var method in methods) + { + if (method.IsGenericMethod) + continue; + + 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); + } + } + + public static IEnumerable<GodotPropertyData> WhereIsGodotCompatibleType( + this IEnumerable<IPropertySymbol> 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.ConvertManagedTypeToMarshalType(property.Type, typeCache); + + if (marshalType == null) + continue; + + yield return new GodotPropertyData(property, marshalType.Value); + } + } + + public static IEnumerable<GodotFieldData> WhereIsGodotCompatibleType( + this IEnumerable<IFieldSymbol> fields, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var field in fields) + { + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(field.Type, typeCache); + + if (marshalType == null) + continue; + + yield return new GodotFieldData(field, marshalType.Value); + } + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj index 791ad85572..d61d9f7f14 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>8.0</LangVersion> + <LangVersion>9.0</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs index 7cc8fa17fc..0ea1b2f5ce 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs @@ -4,5 +4,8 @@ namespace Godot.SourceGenerators { public const string Object = "Godot.Object"; public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute"; + public const string ExportAttr = "Godot.ExportAttribute"; + public const string GodotClassNameAttr = "Godot.GodotClassName"; + public const string SystemFlagsAttr = "System.FlagsAttribute"; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs new file mode 100644 index 0000000000..99d3a49546 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs @@ -0,0 +1,134 @@ +using System; + +namespace Godot.SourceGenerators +{ + internal enum VariantType + { + Nil = 0, + Bool = 1, + Int = 2, + Float = 3, + String = 4, + Vector2 = 5, + Vector2i = 6, + Rect2 = 7, + Rect2i = 8, + Vector3 = 9, + Vector3i = 10, + Transform2d = 11, + Vector4 = 12, + Vector4i = 13, + Plane = 14, + Quaternion = 15, + Aabb = 16, + Basis = 17, + Transform3d = 18, + Projection = 19, + Color = 20, + StringName = 21, + NodePath = 22, + Rid = 23, + Object = 24, + Callable = 25, + Signal = 26, + Dictionary = 27, + Array = 28, + PackedByteArray = 29, + PackedInt32Array = 30, + PackedInt64Array = 31, + PackedFloat32Array = 32, + PackedFloat64Array = 33, + PackedStringArray = 34, + PackedVector2Array = 35, + PackedVector3Array = 36, + PackedColorArray = 37, + Max = 38 + } + + internal enum PropertyHint + { + None = 0, + Range = 1, + Enum = 2, + EnumSuggestion = 3, + ExpEasing = 4, + Link = 5, + Flags = 6, + Layers2dRender = 7, + Layers2dPhysics = 8, + Layers2dNavigation = 9, + Layers3dRender = 10, + Layers3dPhysics = 11, + Layers3dNavigation = 12, + File = 13, + Dir = 14, + GlobalFile = 15, + GlobalDir = 16, + ResourceType = 17, + MultilineText = 18, + Expression = 19, + PlaceholderText = 20, + ColorNoAlpha = 21, + ImageCompressLossy = 22, + ImageCompressLossless = 23, + ObjectId = 24, + TypeString = 25, + NodePathToEditedNode = 26, + MethodOfVariantType = 27, + MethodOfBaseType = 28, + MethodOfInstance = 29, + MethodOfScript = 30, + PropertyOfVariantType = 31, + PropertyOfBaseType = 32, + PropertyOfInstance = 33, + PropertyOfScript = 34, + ObjectTooBig = 35, + NodePathValidTypes = 36, + SaveFile = 37, + GlobalSaveFile = 38, + IntIsObjectid = 39, + IntIsPointer = 41, + ArrayType = 40, + LocaleId = 42, + LocalizableString = 43, + NodeType = 44, + Max = 45 + } + + [Flags] + internal enum PropertyUsageFlags + { + None = 0, + Storage = 2, + Editor = 4, + Checkable = 8, + Checked = 16, + Internationalized = 32, + Group = 64, + Category = 128, + Subgroup = 256, + ClassIsBitfield = 512, + NoInstanceState = 1024, + RestartIfChanged = 2048, + ScriptVariable = 4096, + StoreIfNull = 8192, + AnimateAsTrigger = 16384, + UpdateAllIfModified = 32768, + ScriptDefaultValue = 65536, + ClassIsEnum = 131072, + NilIsVariant = 262144, + Internal = 524288, + DoNotShareOnDuplicate = 1048576, + HighEndGfx = 2097152, + NodePathFromSceneRoot = 4194304, + ResourceNotPersistent = 8388608, + KeyingIncrements = 16777216, + DeferredSetResource = 33554432, + EditorInstantiateObject = 67108864, + EditorBasicSetting = 134217728, + Array = 536870912, + Default = 6, + DefaultIntl = 38, + NoEditor = 2 + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs new file mode 100644 index 0000000000..ff640a7a96 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + public struct GodotMethodData + { + public GodotMethodData(IMethodSymbol method, ImmutableArray<MarshalType> paramTypes, + ImmutableArray<ITypeSymbol> paramTypeSymbols, MarshalType? retType) + { + Method = method; + ParamTypes = paramTypes; + ParamTypeSymbols = paramTypeSymbols; + RetType = retType; + } + + public IMethodSymbol Method { get; } + public ImmutableArray<MarshalType> ParamTypes { get; } + public ImmutableArray<ITypeSymbol> ParamTypeSymbols { get; } + public MarshalType? RetType { get; } + } + + public struct GodotPropertyData + { + public GodotPropertyData(IPropertySymbol propertySymbol, MarshalType type) + { + PropertySymbol = propertySymbol; + Type = type; + } + + public IPropertySymbol PropertySymbol { get; } + public MarshalType Type { get; } + } + + public struct GodotFieldData + { + public GodotFieldData(IFieldSymbol fieldSymbol, MarshalType type) + { + FieldSymbol = fieldSymbol; + Type = type; + } + + public IFieldSymbol FieldSymbol { get; } + public MarshalType Type { get; } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs index 7c8345d16a..1c4c19569e 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs @@ -30,6 +30,9 @@ namespace Godot.SourceGenerators Basis, Quaternion, Transform3D, + Vector4, + Vector4i, + Projection, AABB, Color, Plane, diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index a77e1800fb..5a4badd66e 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -1,9 +1,10 @@ using System; +using System.Linq; using Microsoft.CodeAnalysis; namespace Godot.SourceGenerators { - public static class MarshalUtils + internal static class MarshalUtils { public class TypeCache { @@ -35,7 +36,73 @@ namespace Godot.SourceGenerators } } - public static MarshalType? ConvertManagedTypeToVariantType(ITypeSymbol type, TypeCache typeCache) + public static VariantType? ConvertMarshalTypeToVariantType(MarshalType marshalType) + => marshalType switch + { + MarshalType.Boolean => VariantType.Bool, + MarshalType.Char => VariantType.Int, + MarshalType.SByte => VariantType.Int, + MarshalType.Int16 => VariantType.Int, + MarshalType.Int32 => VariantType.Int, + MarshalType.Int64 => VariantType.Int, + MarshalType.Byte => VariantType.Int, + MarshalType.UInt16 => VariantType.Int, + MarshalType.UInt32 => VariantType.Int, + MarshalType.UInt64 => VariantType.Int, + MarshalType.Single => VariantType.Float, + MarshalType.Double => VariantType.Float, + MarshalType.String => VariantType.String, + MarshalType.Vector2 => VariantType.Vector2, + MarshalType.Vector2i => VariantType.Vector2i, + MarshalType.Rect2 => VariantType.Rect2, + MarshalType.Rect2i => VariantType.Rect2i, + MarshalType.Transform2D => VariantType.Transform2d, + MarshalType.Vector3 => VariantType.Vector3, + MarshalType.Vector3i => VariantType.Vector3i, + MarshalType.Basis => VariantType.Basis, + MarshalType.Quaternion => VariantType.Quaternion, + MarshalType.Transform3D => VariantType.Transform3d, + MarshalType.Vector4 => VariantType.Vector4, + MarshalType.Vector4i => VariantType.Vector4i, + MarshalType.Projection => VariantType.Projection, + MarshalType.AABB => VariantType.Aabb, + MarshalType.Color => VariantType.Color, + MarshalType.Plane => VariantType.Plane, + MarshalType.Callable => VariantType.Callable, + MarshalType.SignalInfo => VariantType.Signal, + MarshalType.Enum => VariantType.Int, + MarshalType.ByteArray => VariantType.PackedByteArray, + MarshalType.Int32Array => VariantType.PackedInt32Array, + MarshalType.Int64Array => VariantType.PackedInt64Array, + MarshalType.SingleArray => VariantType.PackedFloat32Array, + MarshalType.DoubleArray => VariantType.PackedFloat64Array, + MarshalType.StringArray => VariantType.PackedStringArray, + MarshalType.Vector2Array => VariantType.PackedVector2Array, + MarshalType.Vector3Array => VariantType.PackedVector3Array, + MarshalType.ColorArray => VariantType.PackedColorArray, + MarshalType.GodotObjectOrDerivedArray => VariantType.Array, + MarshalType.SystemObjectArray => VariantType.Array, + MarshalType.GodotGenericDictionary => VariantType.Dictionary, + MarshalType.GodotGenericArray => VariantType.Array, + MarshalType.SystemGenericDictionary => VariantType.Dictionary, + MarshalType.SystemGenericList => VariantType.Array, + MarshalType.GenericIDictionary => VariantType.Dictionary, + MarshalType.GenericICollection => VariantType.Array, + MarshalType.GenericIEnumerable => VariantType.Array, + MarshalType.SystemObject => VariantType.Nil, + MarshalType.GodotObjectOrDerived => VariantType.Object, + MarshalType.StringName => VariantType.StringName, + MarshalType.NodePath => VariantType.NodePath, + MarshalType.RID => VariantType.Rid, + MarshalType.GodotDictionary => VariantType.Dictionary, + MarshalType.GodotArray => VariantType.Array, + MarshalType.IDictionary => VariantType.Dictionary, + MarshalType.ICollection => VariantType.Array, + MarshalType.IEnumerable => VariantType.Array, + _ => null + }; + + public static MarshalType? ConvertManagedTypeToMarshalType(ITypeSymbol type, TypeCache typeCache) { var specialType = type.SpecialType; @@ -69,39 +136,44 @@ namespace Godot.SourceGenerators return MarshalType.String; case SpecialType.System_Object: return MarshalType.SystemObject; - case SpecialType.System_ValueType: + default: { - if (type.ContainingAssembly.Name == "GodotSharp" && - type.ContainingNamespace.Name == "Godot") + var typeKind = type.TypeKind; + + if (typeKind == TypeKind.Enum) + return MarshalType.Enum; + + if (typeKind == TypeKind.Struct) { - return type switch + if (type.ContainingAssembly.Name == "GodotSharp" && + type.ContainingNamespace.Name == "Godot") { - { Name: "Vector2" } => MarshalType.Vector2, - { Name: "Vector2i" } => MarshalType.Vector2i, - { Name: "Rect2" } => MarshalType.Rect2, - { Name: "Rect2i" } => MarshalType.Rect2i, - { Name: "Transform2D" } => MarshalType.Transform2D, - { Name: "Vector3" } => MarshalType.Vector3, - { Name: "Vector3i" } => MarshalType.Vector3i, - { Name: "Basis" } => MarshalType.Basis, - { Name: "Quaternion" } => MarshalType.Quaternion, - { Name: "Transform3D" } => MarshalType.Transform3D, - { Name: "AABB" } => MarshalType.AABB, - { Name: "Color" } => MarshalType.Color, - { Name: "Plane" } => MarshalType.Plane, - { Name: "RID" } => MarshalType.RID, - { Name: "Callable" } => MarshalType.Callable, - { Name: "SignalInfo" } => MarshalType.SignalInfo, - { TypeKind: TypeKind.Enum } => MarshalType.Enum, - _ => null - }; + return type switch + { + { Name: "Vector2" } => MarshalType.Vector2, + { Name: "Vector2i" } => MarshalType.Vector2i, + { Name: "Rect2" } => MarshalType.Rect2, + { Name: "Rect2i" } => MarshalType.Rect2i, + { Name: "Transform2D" } => MarshalType.Transform2D, + { Name: "Vector3" } => MarshalType.Vector3, + { Name: "Vector3i" } => MarshalType.Vector3i, + { Name: "Basis" } => MarshalType.Basis, + { Name: "Quaternion" } => MarshalType.Quaternion, + { Name: "Transform3D" } => MarshalType.Transform3D, + { Name: "Vector4" } => MarshalType.Vector4, + { Name: "Vector4i" } => MarshalType.Vector4i, + { Name: "Projection" } => MarshalType.Projection, + { Name: "AABB" } => MarshalType.AABB, + { Name: "Color" } => MarshalType.Color, + { Name: "Plane" } => MarshalType.Plane, + { Name: "RID" } => MarshalType.RID, + { Name: "Callable" } => MarshalType.Callable, + { Name: "SignalInfo" } => MarshalType.SignalInfo, + _ => null + }; + } } - - return null; - } - default: - { - if (type.TypeKind == TypeKind.Array) + else if (typeKind == TypeKind.Array) { var arrayType = (IArrayTypeSymbol)type; var elementType = arrayType.ElementType; @@ -127,17 +199,24 @@ namespace Godot.SourceGenerators if (elementType.SimpleDerivesFrom(typeCache.GodotObjectType)) return MarshalType.GodotObjectOrDerivedArray; - if (type.ContainingAssembly.Name == "GodotSharp" && - type.ContainingNamespace.Name == "Godot") + if (elementType.ContainingAssembly.Name == "GodotSharp" && + elementType.ContainingNamespace.Name == "Godot") { - return elementType switch + switch (elementType) { - { Name: "Vector2" } => MarshalType.Vector2Array, - { Name: "Vector3" } => MarshalType.Vector3Array, - { Name: "Color" } => MarshalType.ColorArray, - _ => null - }; + case { Name: "Vector2" }: + return MarshalType.Vector2Array; + case { Name: "Vector3" }: + return MarshalType.Vector3Array; + case { Name: "Color" }: + return MarshalType.ColorArray; + } } + + if (ConvertManagedTypeToMarshalType(elementType, typeCache) != null) + return MarshalType.GodotArray; + + return null; } else if (type is INamedTypeSymbol { IsGenericType: true } genericType) { @@ -190,7 +269,10 @@ namespace Godot.SourceGenerators { Name: "NodePath" } => MarshalType.NodePath, _ => null }; - case "Godot.Collections" when !(type is INamedTypeSymbol { IsGenericType: true }): + case "Collections" + when !(type is INamedTypeSymbol { IsGenericType: true }) && + type.ContainingNamespace.FullQualifiedName() == + "Godot.Collections": return type switch { { Name: "Dictionary" } => MarshalType.GodotDictionary, @@ -220,5 +302,19 @@ namespace Godot.SourceGenerators return false; } + + public static ITypeSymbol? GetArrayElementType(ITypeSymbol typeSymbol) + { + if (typeSymbol.TypeKind == TypeKind.Array) + { + var arrayType = (IArrayTypeSymbol)typeSymbol; + return arrayType.ElementType; + } + + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType) + return genericType.TypeArguments.FirstOrDefault(); + + return null; + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs index 51e9406c15..6d3d03c495 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -9,7 +7,7 @@ using Microsoft.CodeAnalysis.Text; namespace Godot.SourceGenerators { [Generator] - public class ScriptBoilerplateGenerator : ISourceGenerator + public class ScriptMemberInvokerGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { @@ -61,8 +59,6 @@ namespace Godot.SourceGenerators INamedTypeSymbol symbol ) { - string className = symbol.Name; - INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? namespaceSymbol.FullQualifiedName() : @@ -71,9 +67,8 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; - string uniqueName = hasNamespace ? - classNs + "." + className + "_ScriptBoilerplate_Generated" : - className + "_ScriptBoilerplate_Generated"; + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptMemberInvoker_Generated"; var source = new StringBuilder(); @@ -97,7 +92,7 @@ namespace Godot.SourceGenerators source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); - source.Append(containingType.Name); + source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); containingType = containingType.ContainingType; @@ -105,7 +100,7 @@ namespace Godot.SourceGenerators } source.Append("partial class "); - source.Append(className); + source.Append(symbol.NameWithTypeParameters()); source.Append("\n{\n"); var members = symbol.GetMembers(); @@ -113,27 +108,28 @@ namespace Godot.SourceGenerators // TODO: Static static marshaling (no reflection, no runtime type checks) var methodSymbols = members - .Where(s => s.Kind == SymbolKind.Method) + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared) .Cast<IMethodSymbol>() - .Where(m => m.MethodKind == MethodKind.Ordinary && !m.IsImplicitlyDeclared); + .Where(m => m.MethodKind == MethodKind.Ordinary); var propertySymbols = members - .Where(s => s.Kind == SymbolKind.Property) + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) .Cast<IPropertySymbol>(); var fieldSymbols = members - .Where(s => s.Kind == SymbolKind.Field) - .Cast<IFieldSymbol>() - .Where(p => !p.IsImplicitlyDeclared); + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast<IFieldSymbol>(); - var godotClassMethods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray(); - var godotClassProperties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray(); - var godotClassFields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray(); + var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache).ToArray(); + var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); + var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); - source.Append(" private class GodotInternal {\n"); + 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; @@ -144,26 +140,6 @@ namespace Godot.SourceGenerators 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 @@ -191,8 +167,8 @@ namespace Godot.SourceGenerators // Setters - bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.Field.IsReadOnly) && - godotClassProperties.All(pi => pi.Property.IsReadOnly); + bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) && + godotClassProperties.All(pi => pi.PropertySymbol.IsReadOnly); if (!allPropertiesAreReadOnly) { @@ -202,21 +178,21 @@ namespace Godot.SourceGenerators isFirstEntry = true; foreach (var property in godotClassProperties) { - if (property.Property.IsReadOnly) + if (property.PropertySymbol.IsReadOnly) continue; - GeneratePropertySetter(property.Property.Name, - property.Property.Type.FullQualifiedName(), source, isFirstEntry); + GeneratePropertySetter(property.PropertySymbol.Name, + property.PropertySymbol.Type.FullQualifiedName(), source, isFirstEntry); isFirstEntry = false; } foreach (var field in godotClassFields) { - if (field.Field.IsReadOnly) + if (field.FieldSymbol.IsReadOnly) continue; - GeneratePropertySetter(field.Field.Name, - field.Field.Type.FullQualifiedName(), source, isFirstEntry); + GeneratePropertySetter(field.FieldSymbol.Name, + field.FieldSymbol.Type.FullQualifiedName(), source, isFirstEntry); isFirstEntry = false; } @@ -233,13 +209,13 @@ namespace Godot.SourceGenerators isFirstEntry = true; foreach (var property in godotClassProperties) { - GeneratePropertyGetter(property.Property.Name, source, isFirstEntry); + GeneratePropertyGetter(property.PropertySymbol.Name, source, isFirstEntry); isFirstEntry = false; } foreach (var field in godotClassFields) { - GeneratePropertyGetter(field.Field.Name, source, isFirstEntry); + GeneratePropertyGetter(field.FieldSymbol.Name, source, isFirstEntry); isFirstEntry = false; } @@ -285,11 +261,11 @@ namespace Godot.SourceGenerators source.Append("\n}\n"); } - context.AddSource(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8)); + context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); } private static void GenerateMethodInvoker( - GodotMethodInfo method, + GodotMethodData method, StringBuilder source ) { @@ -399,7 +375,7 @@ namespace Godot.SourceGenerators } private static void GenerateHasMethodEntry( - GodotMethodInfo method, + GodotMethodData method, StringBuilder source, bool isFirstEntry ) @@ -417,118 +393,5 @@ namespace Godot.SourceGenerators public void Initialize(GeneratorInitializationContext context) { } - - private struct GodotMethodInfo - { - public GodotMethodInfo(IMethodSymbol method, ImmutableArray<MarshalType> paramTypes, - ImmutableArray<ITypeSymbol> paramTypeSymbols, MarshalType? retType) - { - Method = method; - ParamTypes = paramTypes; - ParamTypeSymbols = paramTypeSymbols; - RetType = retType; - } - - public IMethodSymbol Method { get; } - public ImmutableArray<MarshalType> ParamTypes { get; } - public ImmutableArray<ITypeSymbol> 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<GodotMethodInfo> WhereHasCompatibleGodotType( - IEnumerable<IMethodSymbol> 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<MarshalType>().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<GodotPropertyInfo> WhereIsCompatibleGodotType( - IEnumerable<IPropertySymbol> 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<GodotFieldInfo> WhereIsCompatibleGodotType( - IEnumerable<IFieldSymbol> 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); - } - } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index b625287087..e8a9e28d0c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -90,21 +90,14 @@ namespace Godot.SourceGenerators attributes.Append(@""")]"); } - string className = symbol.Name; - INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? namespaceSymbol.FullQualifiedName() : string.Empty; bool hasNamespace = classNs.Length != 0; - var uniqueName = new StringBuilder(); - if (hasNamespace) - uniqueName.Append($"{classNs}."); - uniqueName.Append(className); - if (symbol.IsGenericType) - uniqueName.Append($"Of{string.Join(string.Empty, symbol.TypeParameters)}"); - uniqueName.Append("_ScriptPath_Generated"); + var uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptPath_Generated"; var source = new StringBuilder(); @@ -124,10 +117,8 @@ namespace Godot.SourceGenerators } source.Append(attributes); - source.Append("\n partial class "); - source.Append(className); - if (symbol.IsGenericType) - source.Append($"<{string.Join(", ", symbol.TypeParameters)}>"); + source.Append("\npartial class "); + source.Append(symbol.NameWithTypeParameters()); source.Append("\n{\n}\n"); if (hasNamespace) @@ -135,7 +126,7 @@ namespace Godot.SourceGenerators source.Append("\n}\n"); } - context.AddSource(uniqueName.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); + context.AddSource(uniqueHint.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); } private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context, diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs new file mode 100644 index 0000000000..85fa65d1af --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -0,0 +1,527 @@ +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class ScriptPropertiesGenerator : 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); + } + } + } + + 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() + + "_ScriptProperties_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>(); + + 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(); + + source.Append(" private partial class GodotInternal {\n"); + + // Generate cached StringNames for methods and properties, for fast lookup + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.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.FieldSymbol.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 GetGodotPropertiesMetadata + + if (godotClassProperties.Length > 0 || godotClassFields.Length > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + string dictionaryType = "System.Collections.Generic.List<Godot.Bridge.PropertyInfo>"; + + source.Append(" internal new static ") + .Append(dictionaryType) + .Append(" GetGodotPropertiesMetadata()\n {\n"); + + source.Append(" var properties = new ") + .Append(dictionaryType) + .Append("();\n"); + + foreach (var property in godotClassProperties) + { + var propertyInfo = GetPropertyMetadata(context, typeCache, + property.PropertySymbol, property.Type); + + if (propertyInfo == null) + continue; + + AppendPropertyInfo(source, propertyInfo.Value); + } + + foreach (var field in godotClassFields) + { + var propertyInfo = GetPropertyMetadata(context, typeCache, + field.FieldSymbol, field.Type); + + if (propertyInfo == null) + continue; + + AppendPropertyInfo(source, propertyInfo.Value); + } + + source.Append(" return properties;\n"); + source.Append(" }\n"); + + source.Append("#pragma warning restore CS0109\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 AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) + { + source.Append(" properties.Add(new Godot.Bridge.PropertyInfo(type: (Godot.Variant.Type)") + .Append((int)propertyInfo.Type) + .Append(", name: GodotInternal.PropName_") + .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("));\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( + GeneratorExecutionContext context, + MarshalUtils.TypeCache typeCache, + ISymbol memberSymbol, + MarshalType marshalType + ) + { + var exportAttr = memberSymbol.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false); + + var propertySymbol = memberSymbol as IPropertySymbol; + var fieldSymbol = memberSymbol as IFieldSymbol; + + if (exportAttr != null && propertySymbol != null) + { + if (propertySymbol.GetMethod == null) + { + // This should never happen, as we filtered WriteOnly properties, but just in case. + Common.ReportExportedMemberIsWriteOnly(context, propertySymbol); + return null; + } + + if (propertySymbol.SetMethod == null) + { + // This should never happen, as we filtered ReadOnly properties, but just in case. + Common.ReportExportedMemberIsReadOnly(context, propertySymbol); + return null; + } + } + + var memberType = propertySymbol?.Type ?? fieldSymbol!.Type; + + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + string memberName = memberSymbol.Name; + + if (exportAttr == null) + { + return new PropertyInfo(memberVariantType, memberName, PropertyHint.None, + hintString: null, PropertyUsageFlags.ScriptVariable, exported: false); + } + + if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType, + isTypeArgument: false, out var hint, out var hintString)) + { + var constructorArguments = exportAttr.ConstructorArguments; + + if (constructorArguments.Length > 0) + { + var hintValue = exportAttr.ConstructorArguments[0].Value; + + hint = hintValue switch + { + null => PropertyHint.None, + int intValue => (PropertyHint)intValue, + _ => (PropertyHint)(long)hintValue + }; + + hintString = constructorArguments.Length > 1 ? + exportAttr.ConstructorArguments[1].Value?.ToString() : + null; + } + else + { + hint = PropertyHint.None; + } + } + + var propUsage = PropertyUsageFlags.Default | PropertyUsageFlags.ScriptVariable; + + if (memberVariantType == VariantType.Nil) + propUsage |= PropertyUsageFlags.NilIsVariant; + + return new PropertyInfo(memberVariantType, memberName, + hint, hintString, propUsage, exported: true); + } + + private static bool TryGetMemberExportHint( + MarshalUtils.TypeCache typeCache, + ITypeSymbol type, AttributeData exportAttr, + VariantType variantType, bool isTypeArgument, + out PropertyHint hint, out string? hintString + ) + { + hint = PropertyHint.None; + hintString = null; + + if (variantType == VariantType.Nil) + return true; // Variant, no export hint + + if (variantType == VariantType.Int && + type.IsValueType && type.TypeKind == TypeKind.Enum) + { + bool hasFlagsAttr = type.GetAttributes() + .Any(a => a.AttributeClass?.IsSystemFlagsAttribute() ?? false); + + hint = hasFlagsAttr ? PropertyHint.Flags : PropertyHint.Enum; + + var members = type.GetMembers(); + + var enumFields = members + .Where(s => s.Kind == SymbolKind.Field && s.IsStatic && + s.DeclaredAccessibility == Accessibility.Public && + !s.IsImplicitlyDeclared) + .Cast<IFieldSymbol>().ToArray(); + + var hintStringBuilder = new StringBuilder(); + var nameOnlyHintStringBuilder = new StringBuilder(); + + // True: enum Foo { Bar, Baz, Qux } + // True: enum Foo { Bar = 0, Baz = 1, Qux = 2 } + // False: enum Foo { Bar = 0, Baz = 7, Qux = 5 } + bool usesDefaultValues = true; + + for (int i = 0; i < enumFields.Length; i++) + { + var enumField = enumFields[i]; + + if (i > 0) + { + hintStringBuilder.Append(","); + nameOnlyHintStringBuilder.Append(","); + } + + string enumFieldName = enumField.Name; + hintStringBuilder.Append(enumFieldName); + nameOnlyHintStringBuilder.Append(enumFieldName); + + long val = enumField.ConstantValue switch + { + sbyte v => v, + short v => v, + int v => v, + long v => v, + byte v => v, + ushort v => v, + uint v => v, + ulong v => (long)v, + _ => 0 + }; + + uint expectedVal = (uint)(hint == PropertyHint.Flags ? 1 << i : i); + if (val != expectedVal) + usesDefaultValues = false; + + hintStringBuilder.Append(":"); + hintStringBuilder.Append(val); + } + + hintString = !usesDefaultValues ? + hintStringBuilder.ToString() : + // If we use the format NAME:VAL, that's what the editor displays. + // That's annoying if the user is not using custom values for the enum constants. + // This may not be needed in the future if the editor is changed to not display values. + nameOnlyHintStringBuilder.ToString(); + + return true; + } + + if (variantType == VariantType.Object && type is INamedTypeSymbol memberNamedType) + { + if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Resource")) + { + string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!; + + hint = PropertyHint.ResourceType; + hintString = nativeTypeName; + + return true; + } + + if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Node")) + { + string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!; + + hint = PropertyHint.NodeType; + hintString = nativeTypeName; + + return true; + } + } + + static bool GetStringArrayEnumHint(VariantType elementVariantType, + AttributeData exportAttr, out string? hintString) + { + var constructorArguments = exportAttr.ConstructorArguments; + + if (constructorArguments.Length > 0) + { + var presetHintValue = exportAttr.ConstructorArguments[0].Value; + + PropertyHint presetHint = presetHintValue switch + { + null => PropertyHint.None, + int intValue => (PropertyHint)intValue, + _ => (PropertyHint)(long)presetHintValue + }; + + if (presetHint == PropertyHint.Enum) + { + string? presetHintString = constructorArguments.Length > 1 ? + exportAttr.ConstructorArguments[1].Value?.ToString() : + null; + + hintString = (int)elementVariantType + "/" + (int)PropertyHint.Enum + ":"; + + if (presetHintString != null) + hintString += presetHintString; + + return true; + } + } + + hintString = null; + return false; + } + + if (!isTypeArgument && variantType == VariantType.Array) + { + var elementType = MarshalUtils.GetArrayElementType(type); + + if (elementType == null) + return false; // Non-generic Array, so there's no hint to add + + var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache)!.Value; + var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType)!.Value; + + bool isPresetHint = false; + + if (elementVariantType == VariantType.String) + isPresetHint = GetStringArrayEnumHint(elementVariantType, exportAttr, out hintString); + + if (!isPresetHint) + { + bool hintRes = TryGetMemberExportHint(typeCache, elementType, + exportAttr, elementVariantType, isTypeArgument: true, + out var elementHint, out var elementHintString); + + // Format: type/hint:hint_string + if (hintRes) + { + hintString = (int)elementVariantType + "/" + (int)elementHint + ":"; + + if (elementHintString != null) + hintString += elementHintString; + } + else + { + hintString = (int)elementVariantType + "/" + (int)PropertyHint.None + ":"; + } + } + + hint = PropertyHint.TypeString; + + return hintString != null; + } + + if (!isTypeArgument && variantType == VariantType.PackedStringArray) + { + if (GetStringArrayEnumHint(VariantType.String, exportAttr, out hintString)) + { + hint = PropertyHint.TypeString; + return true; + } + } + + if (!isTypeArgument && variantType == VariantType.Dictionary) + { + // TODO: Dictionaries are not supported in the inspector + return false; + } + + return false; + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs new file mode 100644 index 0000000000..3b8ba21107 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs @@ -0,0 +1,293 @@ +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 ScriptPropertyDefValGenerator : 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); + } + } + } + + 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() + + "_ScriptPropertyDefVal_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 exportedMembers = new List<ExportedPropertyMetadata>(); + + var members = symbol.GetMembers(); + + var exportedProperties = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) + .Cast<IPropertySymbol>() + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) + .ToArray(); + + var exportedFields = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast<IFieldSymbol>() + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) + .ToArray(); + + foreach (var property in exportedProperties) + { + if (property.IsStatic) + { + Common.ReportExportedMemberIsStatic(context, property); + continue; + } + + // TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (property.IsWriteOnly) + { + Common.ReportExportedMemberIsWriteOnly(context, property); + continue; + } + + if (property.IsReadOnly) + { + Common.ReportExportedMemberIsReadOnly(context, property); + continue; + } + + + var propertyType = property.Type; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(propertyType, typeCache); + + if (marshalType == null) + { + Common.ReportExportedMemberTypeNotSupported(context, property); + continue; + } + + // TODO: Detect default value from simple property getters (currently we only detect from initializers) + + EqualsValueClauseSyntax? initializer = property.DeclaringSyntaxReferences + .Select(r => r.GetSyntax() as PropertyDeclarationSyntax) + .Select(s => s?.Initializer ?? null) + .FirstOrDefault(); + + string? value = initializer?.Value.ToString(); + + exportedMembers.Add(new ExportedPropertyMetadata( + property.Name, marshalType.Value, propertyType, value)); + } + + foreach (var field in exportedFields) + { + if (field.IsStatic) + { + Common.ReportExportedMemberIsStatic(context, field); + continue; + } + + // TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (field.IsReadOnly) + { + Common.ReportExportedMemberIsReadOnly(context, field); + continue; + } + + var fieldType = field.Type; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(fieldType, typeCache); + + if (marshalType == null) + { + Common.ReportExportedMemberTypeNotSupported(context, field); + continue; + } + + EqualsValueClauseSyntax? initializer = field.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) + .OfType<VariableDeclaratorSyntax>() + .Select(s => s.Initializer) + .FirstOrDefault(i => i != null); + + string? value = initializer?.Value.ToString(); + + exportedMembers.Add(new ExportedPropertyMetadata( + field.Name, marshalType.Value, fieldType, value)); + } + + // Generate GetGodotExportedProperties + + if (exportedMembers.Count > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + string dictionaryType = "System.Collections.Generic.Dictionary<StringName, object>"; + + source.Append("#if TOOLS\n"); + source.Append(" internal new static "); + source.Append(dictionaryType); + source.Append(" GetGodotPropertyDefaultValues()\n {\n"); + + source.Append(" var values = new "); + source.Append(dictionaryType); + source.Append("("); + source.Append(exportedMembers.Count); + source.Append(");\n"); + + foreach (var exportedMember in exportedMembers) + { + string defaultValueLocalName = string.Concat("__", exportedMember.Name, "_default_value"); + + source.Append(" "); + source.Append(exportedMember.TypeSymbol.FullQualifiedName()); + source.Append(" "); + source.Append(defaultValueLocalName); + source.Append(" = "); + source.Append(exportedMember.Value ?? "default"); + source.Append(";\n"); + source.Append(" values.Add(GodotInternal.PropName_"); + source.Append(exportedMember.Name); + source.Append(", "); + source.Append(defaultValueLocalName); + source.Append(");\n"); + } + + source.Append(" return values;\n"); + source.Append(" }\n"); + source.Append("#endif\n"); + + source.Append("#pragma warning restore CS0109\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 struct ExportedPropertyMetadata + { + public ExportedPropertyMetadata(string name, MarshalType type, ITypeSymbol typeSymbol, string? value) + { + Name = name; + Type = type; + TypeSymbol = typeSymbol; + Value = value; + } + + public string Name { get; } + public MarshalType Type { get; } + public ITypeSymbol TypeSymbol { get; } + public string? Value { get; } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 4e27f4ed14..fe83e6a281 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -513,20 +513,23 @@ namespace GodotTools protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_exportPluginWeak != null) + if (disposing) { - // We need to dispose our export plugin before the editor destroys EditorSettings. - // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid - // will be freed after EditorSettings already was, and its device polling thread - // will try to access the EditorSettings singleton, resulting in null dereferencing. - (_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); + if (IsInstanceValid(_exportPluginWeak)) + { + // We need to dispose our export plugin before the editor destroys EditorSettings. + // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid + // will be freed after EditorSettings already was, and its device polling thread + // will try to access the EditorSettings singleton, resulting in null dereferencing. + (_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); - _exportPluginWeak.Dispose(); + _exportPluginWeak.Dispose(); + } + + GodotIdeManager?.Dispose(); } - GodotIdeManager?.Dispose(); + base.Dispose(disposing); } public void OnBeforeSerialize() diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 746cb8a142..6c805c605d 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -1407,6 +1407,13 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str } } + // We generate a `GodotClassName` attribute if the engine class name is not the same as the + // generated C# class name. This allows introspection code to find the name associated with + // the class. If the attribute is not present, the C# class name can be used instead. + if (itype.name != itype.proxy_name) { + output << INDENT1 "[GodotClassName(\"" << itype.name << "\")]\n"; + } + output.append(INDENT1 "public "); if (itype.is_singleton) { output.append("static partial class "); |