From 4c2d0f52ce7fe52714c74a1b3a3985cdb8ae52ad Mon Sep 17 00:00:00 2001 From: Joanna May Date: Mon, 27 Nov 2023 18:41:28 -0600 Subject: [PATCH] fix: wm notifications (#16) * feat: super objects * fix: rename WM notifications * chore: bump version --- README.md | 3 +- .../test/test_cases/AllNotifications.cs | 65 ++++++++ .../test_cases/PlainOldSuperObjectTest.cs | 48 ++++++ SuperNodes.Tests/reports/branch_coverage.svg | 2 +- SuperNodes.Tests/reports/line_coverage.svg | 2 +- .../PowerUpsFeature/PowerUpGeneratorTest.cs | 31 ++-- .../SuperNodeGeneratorTest.cs | 13 +- .../SuperNodesFeature/SuperNodesRepoTest.cs | 2 +- .../tests/SuperNodesGeneratorTest.cs | 8 +- .../tests/common/models/ContainingTypeTest.cs | 71 +++++++++ .../tests/common/models/GenerationItemTest.cs | 35 ++++- .../tests/common/models/SuperNodeTest.cs | 23 ++- .../tests/common/services/CodeServiceTest.cs | 73 +++++++++ SuperNodes.Tests/types_tests/TypesTest.cs | 50 +++--- .../Chickensoft.SuperNodes.Types.csproj | 7 +- SuperNodes.Types/src/Types.cs | 65 ++++++-- SuperNodes/Chickensoft.SuperNodes.csproj | 2 +- .../src/PowerUpsFeature/PowerUpGenerator.cs | 68 +++++--- .../src/PowerUpsFeature/PowerUpsRepo.cs | 37 ++--- .../SuperNodesFeature/SuperNodeGenerator.cs | 36 +++-- .../src/SuperNodesFeature/SuperNodesRepo.cs | 78 +++++++++- SuperNodes/src/SuperNodesGenerator.cs | 121 +++++++++----- .../src/common/models/ContainingType.cs | 42 +++++ .../src/common/models/GenerationItem.cs | 43 ++++- SuperNodes/src/common/models/SuperNode.cs | 147 ++++++++++++++++-- SuperNodes/src/common/services/CodeService.cs | 95 ++++++++--- SuperNodes/src/common/utils/Constants.cs | 38 ++--- 27 files changed, 991 insertions(+), 214 deletions(-) create mode 100644 SuperNodes.TestCases/test/test_cases/AllNotifications.cs create mode 100644 SuperNodes.TestCases/test/test_cases/PlainOldSuperObjectTest.cs create mode 100644 SuperNodes.Tests/tests/common/models/ContainingTypeTest.cs create mode 100644 SuperNodes/src/common/models/ContainingType.cs diff --git a/README.md b/README.md index 1e4ae8e..3e425b1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ SuperNodes is a C# source generator that gives superpowers to Godot node scripts SuperNodes can do a LOT — [check out the official documentation][docs] for usage details. -- ✅ Apply PowerUps (essentially mixins for C#) to any class that extends a Godot `Node` or `GodotObject`. +- ✅ Apply PowerUps (essentially mixins for C#) to any class or record. +- ✅ PowerUps applied to a `GodotObject` or Godot `Node` can hook into the node's lifecycle, observing events and running code before user script callbacks. - ✅ Use third-party source generators alongside Godot's official source generators. - ✅ Get and set the value of script properties and fields at runtime, without using reflection. - ✅ Examine the attributes and types of script properties and fields at runtime, without using reflection. diff --git a/SuperNodes.TestCases/test/test_cases/AllNotifications.cs b/SuperNodes.TestCases/test/test_cases/AllNotifications.cs new file mode 100644 index 0000000..ca0830f --- /dev/null +++ b/SuperNodes.TestCases/test/test_cases/AllNotifications.cs @@ -0,0 +1,65 @@ + +using Godot; +using SuperNodes.Types; + +namespace AllNotifications; + +[SuperNode] +public partial class Example : Node { + private void OnPostinitialize() { } + private void OnPredelete() { } + private void OnNotification(int what) { } + private void OnEnterTree() { } + // not working + private void OnWMWindowFocusIn() { } + // not working + private void OnWMWindowFocusOut() { } + // not working + private void OnWMCloseRequest() { } + // not working + private void OnWMSizeChanged() { } + // not working + private void OnWMDpiChange() { } + private void OnVpMouseEnter() { } + private void OnVpMouseExit() { } + private void OnOsMemoryWarning() { } + private void OnTranslationChanged() { } + // not working + private void OnWMAbout() { } + private void OnCrash() { } + private void OnOsImeUpdate() { } + private void OnApplicationResumed() { } + private void OnApplicationPaused() { } + private void OnApplicationFocusIn() { } + private void OnApplicationFocusOut() { } + private void OnTextServerChanged() { } + // not working + private void OnWMMouseExit() { } + // not working + private void OnWMMouseEnter() { } + // not working + private void OnWMGoBackRequest() { } + private void OnEditorPreSave() { } + private void OnExitTree() { } + private void OnMovedInParent() { } + private void OnReady() { } + private void OnEditorPostSave() { } + private void OnUnpaused() { } + private void OnPhysicsProcess(double delta) { } + private void OnProcess(double delta) { } + private void OnParented() { } + private void OnUnparented() { } + private void OnPaused() { } + private void OnDragBegin() { } + private void OnDragEnd() { } + private void OnPathRenamed() { } + private void OnInternalProcess() { } + private void OnInternalPhysicsProcess() { } + private void OnPostEnterTree() { } + private void OnDisabled() { } + private void OnEnabled() { } + private void OnSceneInstantiated() { } + + + public override partial void _Notification(int what); +} diff --git a/SuperNodes.TestCases/test/test_cases/PlainOldSuperObjectTest.cs b/SuperNodes.TestCases/test/test_cases/PlainOldSuperObjectTest.cs new file mode 100644 index 0000000..12827a4 --- /dev/null +++ b/SuperNodes.TestCases/test/test_cases/PlainOldSuperObjectTest.cs @@ -0,0 +1,48 @@ +namespace PlainOldSuperObjectTest; + +using System; +using Chickensoft.GoDotTest; +using Godot; +using Shouldly; +using SuperNodes.Types; + +public record CommonBaseType { } +public interface IGreeter { } + +[SuperObject(typeof(Greeter))] +public partial record MyPlainOldRecordType : CommonBaseType { } + +public partial class GrandparentType { + public partial class ParentType { + [SuperObject(typeof(Greeter))] + public partial record MyNestedPlainOldRecordType : CommonBaseType { } + } +} + +[PowerUp] +public record Greeter : CommonBaseType, IGreeter { + public void Greet() => Console.WriteLine("Hello, world!"); +} + +public class PlainOldSuperObjectTest : TestClass { + public PlainOldSuperObjectTest(Node testScene) : base(testScene) { } + + [Test] + public void PlainOldObjectsWorkAsSuperObjects() { + var obj = new MyPlainOldRecordType(); + + obj.ShouldBeOfType(); + obj.ShouldBeAssignableTo(); + } + + [Test] + public void NestedPlainOldObjectsWorkAsSuperObjects() { + var obj = + new GrandparentType.ParentType.MyNestedPlainOldRecordType(); + + obj.ShouldBeOfType< + GrandparentType.ParentType.MyNestedPlainOldRecordType + >(); + obj.ShouldBeAssignableTo(); + } +} diff --git a/SuperNodes.Tests/reports/branch_coverage.svg b/SuperNodes.Tests/reports/branch_coverage.svg index f49b59a..21cd0e7 100644 --- a/SuperNodes.Tests/reports/branch_coverage.svg +++ b/SuperNodes.Tests/reports/branch_coverage.svg @@ -94,7 +94,7 @@ - Generated by: ReportGenerator 5.1.17.0 + Generated by: ReportGenerator 5.1.26.0 diff --git a/SuperNodes.Tests/reports/line_coverage.svg b/SuperNodes.Tests/reports/line_coverage.svg index fcdc4e5..e1f11a9 100644 --- a/SuperNodes.Tests/reports/line_coverage.svg +++ b/SuperNodes.Tests/reports/line_coverage.svg @@ -94,7 +94,7 @@ - Generated by: ReportGenerator 5.1.17.0 + Generated by: ReportGenerator 5.1.26.0 diff --git a/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs b/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs index 54cdaba..6a8a57d 100644 --- a/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs +++ b/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs @@ -94,7 +94,15 @@ public void OnTestPowerUp(int what) { HasPartialNotificationMethod: true, HasOnNotificationMethodHandler: true, PropsAndFields: ImmutableArray.Empty, - Usings: ImmutableHashSet.Empty + Usings: ImmutableHashSet.Empty, + ContainingTypes: new ContainingType[] { + new ContainingType( + FullName: "TestContainingType", + Kind: ContainingTypeKind.Class, + Accessibility: Accessibility.Public, + IsPartial: true + ) + }.ToImmutableArray() ); var genService = new Mock(); @@ -136,20 +144,23 @@ public void OnTestPowerUp(int what) { using SuperNodes.Types; namespace Tests { - partial class TestSuperNode : global::Tests.ITestPowerUp - { - public string AddedProperty { get; set; } = "Property"; - private readonly int _addedField = 10; - public static int MyNumber { get; set; } = 42; - public void OnTestPowerUp(int what) + public partial class TestContainingType { + partial class TestSuperNode : global::Tests.ITestPowerUp { - TestSuperNode.MyNumber = 666; - if (what == NotificationReady) + public string AddedProperty { get; set; } = "Property"; + private readonly int _addedField = 10; + public static int MyNumber { get; set; } = 42; + public void OnTestPowerUp(int what) { - GD.Print("Hello, TestPowerUp!"); + TestSuperNode.MyNumber = 666; + if (what == NotificationReady) + { + GD.Print("Hello, TestPowerUp!"); + } } } } + } #nullable disable #pragma warning restore diff --git a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs index 40d8da5..4c8fe0a 100644 --- a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs +++ b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs @@ -28,7 +28,7 @@ public void GeneratesSuperNode() { var notificationHandlers = ImmutableArray.Create(); var powerUps = ImmutableDictionary.Create(); - var item = new GenerationItem( + var item = new SuperNodeGenerationItem( SuperNode: new SuperNode( Namespace: "global::Tests", Name: "TestSuperNode", @@ -41,7 +41,8 @@ public void GeneratesSuperNode() { HasPartialNotificationMethod: true, HasOnNotificationMethodHandler: true, PropsAndFields: ImmutableArray.Empty, - Usings: ImmutableHashSet.Empty + Usings: ImmutableHashSet.Empty, + ContainingTypes: ImmutableArray.Empty ), PowerUps: powerUps ); @@ -73,7 +74,6 @@ public void GeneratesSuperNode() { source.ShouldBe(""" #pragma warning disable #nullable enable - using Godot; using SuperNodes.Types; namespace global::Tests { @@ -194,10 +194,11 @@ public void GeneratesSuperNodeStaticReflectionTables() { HasPartialNotificationMethod: true, HasOnNotificationMethodHandler: true, PropsAndFields: propsAndFields, - Usings: usings + Usings: usings, + ContainingTypes: ImmutableArray.Empty ); - var item = new GenerationItem( + var item = new SuperNodeGenerationItem( SuperNode: superNode, PowerUps: powerUps ); @@ -250,7 +251,7 @@ public void GeneratesSuperNodeStaticReflectionTables() { ) ).Returns(setPropertyOrFieldFn); - var source = generator.GenerateSuperNodeStatic(item, appliedPowerUps); + var source = generator.GenerateStaticReflection(item, appliedPowerUps); source.ShouldBe(""" #pragma warning disable diff --git a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs index 7f86ea5..b05041a 100644 --- a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs +++ b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs @@ -100,7 +100,7 @@ public void OnReady() { } cs => cs.GetAttribute(symbol, Constants.SUPER_NODE_ATTRIBUTE_NAME_FULL) ).Returns((AttributeData?)null); - codeService.Setup(cs => cs.GetLifecycleHooks(null)) + codeService.Setup(cs => cs.GetLifecycleHooks(null, true)) .Returns(LifecycleHooksResponse.Empty); var members = ImmutableArray.Empty; diff --git a/SuperNodes.Tests/tests/SuperNodesGeneratorTest.cs b/SuperNodes.Tests/tests/SuperNodesGeneratorTest.cs index 79ab020..f862929 100644 --- a/SuperNodes.Tests/tests/SuperNodesGeneratorTest.cs +++ b/SuperNodes.Tests/tests/SuperNodesGeneratorTest.cs @@ -32,12 +32,18 @@ public partial class GeneralFeatureSuperNode : Node { public void OnReady() { } } + [SuperObject(typeof(PlainGeneralFeaturePowerUp))] + public partial record PlainObject { } + [PowerUp] public partial class GeneralFeaturePowerUp : Node, IGeneralFeaturePowerUp { T IGeneralFeaturePowerUp.TestValue { get; } = default!; } + [PowerUp] + public record PlainGeneralFeaturePowerUp { } + public interface IGeneralFeaturePowerUp { T TestValue { get; } } @@ -50,7 +56,7 @@ public void OtherGenerator(int what) { } var result = Tester.Generate(source); result.Diagnostics.ShouldBeEmpty(); - result.Outputs.Count.ShouldBe(3); + result.Outputs.Count.ShouldBe(5); } [Fact] diff --git a/SuperNodes.Tests/tests/common/models/ContainingTypeTest.cs b/SuperNodes.Tests/tests/common/models/ContainingTypeTest.cs new file mode 100644 index 0000000..e1731fe --- /dev/null +++ b/SuperNodes.Tests/tests/common/models/ContainingTypeTest.cs @@ -0,0 +1,71 @@ +namespace SuperNodes.Tests.Common.Models; + +using System; +using Shouldly; +using SuperNodes.Common.Models; +using Xunit; + +public class ContainingTypeTest { + [Fact] + public void Initializes() { + var containingType = new ContainingType( + FullName: "TestClass", + Kind: ContainingTypeKind.Record, + Accessibility: Microsoft.CodeAnalysis.Accessibility.Public, + IsPartial: false + ); + + containingType.TypeDeclarationKeyword.ShouldBe("record"); + containingType.AccessibilityKeywords.ShouldBe("public"); + } + + [Fact] + public void GetTypeDeclarationKeyword() { + ContainingType.GetTypeDeclarationKeyword( + ContainingTypeKind.Record + ).ShouldBe("record"); + + ContainingType.GetTypeDeclarationKeyword( + ContainingTypeKind.Class + ).ShouldBe("class"); + + Should.Throw( + () => ContainingType.GetTypeDeclarationKeyword( + (ContainingTypeKind)3 + ) + ); + } + + [Fact] + public void GetAccessibilityKeywords() { + ContainingType.GetAccessibilityKeywords( + Microsoft.CodeAnalysis.Accessibility.Public + ).ShouldBe("public"); + + ContainingType.GetAccessibilityKeywords( + Microsoft.CodeAnalysis.Accessibility.Protected + ).ShouldBe("protected"); + + ContainingType.GetAccessibilityKeywords( + Microsoft.CodeAnalysis.Accessibility.Internal + ).ShouldBe("internal"); + + ContainingType.GetAccessibilityKeywords( + Microsoft.CodeAnalysis.Accessibility.ProtectedOrInternal + ).ShouldBe("protected internal"); + + ContainingType.GetAccessibilityKeywords( + Microsoft.CodeAnalysis.Accessibility.Private + ).ShouldBe("private"); + + ContainingType.GetAccessibilityKeywords( + Microsoft.CodeAnalysis.Accessibility.ProtectedAndInternal + ).ShouldBe("private protected"); + + Should.Throw( + () => ContainingType.GetAccessibilityKeywords( + (Microsoft.CodeAnalysis.Accessibility)7 + ) + ); + } +} diff --git a/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs b/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs index 2b6314b..a9ef05c 100644 --- a/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs +++ b/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs @@ -22,14 +22,39 @@ public void Initializes() { HasPartialNotificationMethod: false, HasOnNotificationMethodHandler: false, PropsAndFields: ImmutableArray.Empty, - Usings: ImmutableHashSet.Empty + Usings: ImmutableHashSet.Empty, + ContainingTypes: ImmutableArray.Empty + ); + + var superObject = new SuperObject( + Namespace: "global::Tests", + Name: "SuperObject", + NameWithoutGenerics: "SuperObject", + Location: new Mock().Object, + BaseClasses: new string[] { "global::Godot.Object" }.ToImmutableArray(), + LifecycleHooks: ImmutableArray.Empty, + PowerUpHooksByFullName: ImmutableDictionary.Empty, + PropsAndFields: ImmutableArray.Empty, + Usings: ImmutableHashSet.Empty, + IsRecord: false, + ContainingTypes: ImmutableArray.Empty ); var powerUps = ImmutableDictionary.Empty; - var generationItem = new GenerationItem(superNode, powerUps); + var superNodeGenerationItem = + new SuperNodeGenerationItem(superNode, powerUps); + + var superObjectGenerationItem = new SuperObjectGenerationItem( + superObject, + powerUps + ); + + superNodeGenerationItem.ShouldBeOfType(); + superNodeGenerationItem.SuperNode.ShouldBe(superNode); + superNodeGenerationItem.PowerUps.ShouldBe(powerUps); - generationItem.ShouldBeOfType(); - generationItem.SuperNode.ShouldBe(superNode); - generationItem.PowerUps.ShouldBe(powerUps); + superObjectGenerationItem.ShouldBeOfType(); + superObjectGenerationItem.SuperObject.ShouldBe(superObject); + superObjectGenerationItem.PowerUps.ShouldBe(powerUps); } } diff --git a/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs b/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs index 9142ef0..7c45c33 100644 --- a/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs +++ b/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs @@ -22,11 +22,29 @@ public void Initializes() { HasPartialNotificationMethod: false, HasOnNotificationMethodHandler: false, PropsAndFields: ImmutableArray.Empty, - Usings: ImmutableHashSet.Empty + Usings: ImmutableHashSet.Empty, + ContainingTypes: ImmutableArray.Empty + ); + + var superObject = new SuperObject( + Namespace: "global::Tests", + Name: "SuperObject", + NameWithoutGenerics: "SuperObject", + Location: new Mock().Object, + BaseClasses: new string[] { "global::Godot.Object" }.ToImmutableArray(), + LifecycleHooks: ImmutableArray.Empty, + PowerUpHooksByFullName: ImmutableDictionary.Empty, + PropsAndFields: ImmutableArray.Empty, + Usings: ImmutableHashSet.Empty, + IsRecord: true, + ContainingTypes: ImmutableArray.Empty ); superNode.ShouldBeOfType(); superNode.FilenamePrefix.ShouldBe("global::Tests.SuperNode"); + + superObject.ShouldBeOfType(); + superObject.FilenamePrefix.ShouldBe("global::Tests.SuperObject"); } [Fact] @@ -43,7 +61,8 @@ public void FilenamePrefixHandlesEmptyNamespace() { HasPartialNotificationMethod: false, HasOnNotificationMethodHandler: false, PropsAndFields: ImmutableArray.Empty, - Usings: ImmutableHashSet.Empty + Usings: ImmutableHashSet.Empty, + ContainingTypes: ImmutableArray.Empty ); superNode.ShouldBeOfType(); diff --git a/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs b/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs index f948956..e1fd0ed 100644 --- a/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs +++ b/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs @@ -2,8 +2,10 @@ namespace SuperNodes.Tests.Common.Services; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using DeepEqual.Syntax; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Moq; using Shouldly; @@ -778,6 +780,77 @@ public class Two { } ) ); } + + [Fact] + public void GetContainingTypesGetsTypes() { + var code = """ + namespace Tests { + using System; + + public partial class TypeA { + public record TypeB { + public partial struct TypeC { } + } + } + } + """; + + var node = Tester.Parse( + code, out var symbol + ); + + var containingTypes = new List() { + new ContainingType( + FullName: "TypeA", + Kind: ContainingTypeKind.Class, + Accessibility: Accessibility.Public, + IsPartial: true + ), + new ContainingType( + FullName: "TypeB", + Kind: ContainingTypeKind.Record, + Accessibility: Accessibility.Public, + IsPartial: true + ), + }.ToImmutableArray(); + + var service = new CodeService(); + + service.GetContainingTypes(null, node).ShouldBeEmpty(); + var result = service.GetContainingTypes(symbol, node); + + result.ShouldDeepEqual(containingTypes); + } + + [Fact] + public void GetContainingTypeCoversNoSyntax() { + var service = new CodeService(); + + var symbol = new Mock(); + var containingSymbol = new Mock(); + + symbol.Setup(s => s.Name).Returns("Test"); + symbol.Setup(s => s.ContainingType).Returns(containingSymbol.Object); + + containingSymbol + .Setup(s => s.DeclaringSyntaxReferences) + .Returns(new List() { + new Mock().Object + }.ToImmutableArray() + ); + + containingSymbol + .Setup(s => s.TypeParameters) + .Returns(ImmutableArray.Create()); + + var fallbackSyntax = SyntaxFactory + .ParseCompilationUnit("class Test { }") + .DescendantNodes().OfType().First(); + + service + .GetContainingTypes(symbol.Object, fallbackSyntax) + .ShouldNotBeEmpty(); + } } internal class TestAttributeData : AttributeData { diff --git a/SuperNodes.Tests/types_tests/TypesTest.cs b/SuperNodes.Tests/types_tests/TypesTest.cs index 9e54aa5..da82deb 100644 --- a/SuperNodes.Tests/types_tests/TypesTest.cs +++ b/SuperNodes.Tests/types_tests/TypesTest.cs @@ -9,6 +9,18 @@ namespace SuperNodes.Types.Tests; using Xunit; public class TypesTest { + [Fact] + public void SuperObjectAttributeDefaultInitializer() { + var attribute = new SuperObjectAttribute(); + attribute.Args.ShouldBeEmpty(); + } + + [Fact] + public void SuperObjectAttributeInitializer() { + var attribute = new SuperObjectAttribute(typeof(int)); + attribute.Args.ShouldBe(new object[] { typeof(int) }); + } + [Fact] public void SuperNodeAttributeDefaultInitializer() { var attribute = new SuperNodeAttribute(); @@ -68,28 +80,28 @@ public void ScriptPropertyOrField() { } } -public class NodeExtensionsTest { +public class ObjectExtensionsTest { [Fact] public void TypeParam() { - NodeExtensions.TypeParam(default!, typeof(int)).ShouldBe("int"); - NodeExtensions.TypeParam(default!, typeof(string)).ShouldBe("string"); - NodeExtensions.TypeParam(default!, typeof(bool)).ShouldBe("bool"); - NodeExtensions.TypeParam(default!, typeof(byte)).ShouldBe("byte"); - NodeExtensions.TypeParam(default!, typeof(sbyte)).ShouldBe("sbyte"); - NodeExtensions.TypeParam(default!, typeof(char)).ShouldBe("char"); - NodeExtensions.TypeParam(default!, typeof(decimal)).ShouldBe("decimal"); - NodeExtensions.TypeParam(default!, typeof(double)).ShouldBe("double"); - NodeExtensions.TypeParam(default!, typeof(float)).ShouldBe("float"); - NodeExtensions.TypeParam(default!, typeof(uint)).ShouldBe("uint"); - NodeExtensions.TypeParam(default!, typeof(nuint)).ShouldBe("nuint"); - NodeExtensions.TypeParam(default!, typeof(long)).ShouldBe("long"); - NodeExtensions.TypeParam(default!, typeof(ulong)).ShouldBe("ulong"); - NodeExtensions.TypeParam(default!, typeof(short)).ShouldBe("short"); - NodeExtensions.TypeParam(default!, typeof(ushort)).ShouldBe("ushort"); - NodeExtensions.TypeParam(default!, typeof(object)).ShouldBe("object"); - NodeExtensions.TypeParam(default!, typeof(TestAttribute)) + ObjectExtensions.TypeParam(default!, typeof(int)).ShouldBe("int"); + ObjectExtensions.TypeParam(default!, typeof(string)).ShouldBe("string"); + ObjectExtensions.TypeParam(default!, typeof(bool)).ShouldBe("bool"); + ObjectExtensions.TypeParam(default!, typeof(byte)).ShouldBe("byte"); + ObjectExtensions.TypeParam(default!, typeof(sbyte)).ShouldBe("sbyte"); + ObjectExtensions.TypeParam(default!, typeof(char)).ShouldBe("char"); + ObjectExtensions.TypeParam(default!, typeof(decimal)).ShouldBe("decimal"); + ObjectExtensions.TypeParam(default!, typeof(double)).ShouldBe("double"); + ObjectExtensions.TypeParam(default!, typeof(float)).ShouldBe("float"); + ObjectExtensions.TypeParam(default!, typeof(uint)).ShouldBe("uint"); + ObjectExtensions.TypeParam(default!, typeof(nuint)).ShouldBe("nuint"); + ObjectExtensions.TypeParam(default!, typeof(long)).ShouldBe("long"); + ObjectExtensions.TypeParam(default!, typeof(ulong)).ShouldBe("ulong"); + ObjectExtensions.TypeParam(default!, typeof(short)).ShouldBe("short"); + ObjectExtensions.TypeParam(default!, typeof(ushort)).ShouldBe("ushort"); + ObjectExtensions.TypeParam(default!, typeof(object)).ShouldBe("object"); + ObjectExtensions.TypeParam(default!, typeof(TestAttribute)) .ShouldBe("global::SuperNodes.Types.Tests.TestAttribute"); - NodeExtensions.TypeParam(default!, new TestType()) + ObjectExtensions.TypeParam(default!, new TestType()) .ShouldBe(nameof(TestType)); } } diff --git a/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj b/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj index 9f0aaf3..284cc30 100644 --- a/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj +++ b/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj @@ -8,10 +8,11 @@ true SuperNodes.Types true + true portable SuperNodes Types - 1.6.1 + 1.7.0 Runtime types for SuperNodes. © 2023 Chickensoft Chickensoft @@ -35,8 +36,4 @@ - - - - diff --git a/SuperNodes.Types/src/Types.cs b/SuperNodes.Types/src/Types.cs index ad11f73..1b0e722 100644 --- a/SuperNodes.Types/src/Types.cs +++ b/SuperNodes.Types/src/Types.cs @@ -3,17 +3,43 @@ namespace SuperNodes.Types; using System; using System.Collections.Generic; using System.Collections.Immutable; -using Godot; /// -/// SuperNode attribute. Add this to a Godot object class to use -/// functionality from other compatible source generators. +/// SuperObject attribute. Add this to a plain C# record or class to mixin +/// functionality from PowerUps (mixins). +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class SuperObjectAttribute : Attribute { + /// + /// PowerUps to "mixin" to the object. + /// + public Type[] Args { get; } + + /// + /// SuperObject attribute. Add this to a Godot object class to use + /// functionality from other compatible source generators. + /// + public SuperObjectAttribute() { + Args = Array.Empty(); + } + + /// + /// SuperObject attribute. Add this to a Godot object class to mixin PowerUps. + /// + /// Types of PowerUps to "mixin" to the object. + public SuperObjectAttribute(params Type[] args) { + Args = args; + } +} + +/// +/// SuperNode attribute. Add this to a Godot object class or script to mixin +/// functionality from PowerUps (mixins). /// [AttributeUsage(AttributeTargets.Class)] public sealed class SuperNodeAttribute : Attribute { /// - /// Source generator lifecycle methods and/or PowerUps to invoke from - /// . + /// PowerUps to "mixin" to the object. /// public object[] Args { get; } @@ -26,13 +52,8 @@ public SuperNodeAttribute() { } /// - /// SuperNode attribute. Add this to a Godot object class to use - /// functionality from other compatible source generators. - ///
- /// Compatible source generator lifecycle methods or PowerUps that will - /// be invoked from - /// in the order - /// specified here. + /// SuperNode attribute. Add this to a Godot object class to mixin PowerUps + /// and/or call the names of methods denoted by strings. ///
/// Compatible source generator lifecycle method /// names or the types of PowerUps. @@ -120,7 +141,10 @@ public record struct ScriptPropertyOrField( IDictionary> Attributes ); -public interface ISuperNode { +/// +/// Base interface of and . +/// +public interface ISuperItem { /// /// A map of all properties and fields in this node script from /// generated identifier name to their type, attribute, and @@ -174,7 +198,18 @@ TResult GetScriptPropertyOrFieldType( void SetScriptPropertyOrField(string scriptProperty, dynamic? value); } -public static class NodeExtensions { +/// +/// Interface added to all super objects. +/// +public interface ISuperObject : ISuperItem { } + +/// +/// Interface added to all super nodes. +/// +public interface ISuperNode : ISuperItem { } + +/// Extensions added to all objects. +public static class ObjectExtensions { /// /// Returns the simple type name of a type parameter's type if the represented /// type is not a built-in type. Otherwise, returns the built-in type name. @@ -188,7 +223,7 @@ public static class NodeExtensions { /// Thrown if an unrecognized /// primitive type is encountered. /// Simple name of the type (or it's built-in type). - public static string TypeParam(this Node node, Type type) { + public static string TypeParam(this object node, Type type) { var fullName = type.FullName; switch (fullName) { case "System.Boolean": diff --git a/SuperNodes/Chickensoft.SuperNodes.csproj b/SuperNodes/Chickensoft.SuperNodes.csproj index 758172d..deb7450 100644 --- a/SuperNodes/Chickensoft.SuperNodes.csproj +++ b/SuperNodes/Chickensoft.SuperNodes.csproj @@ -13,7 +13,7 @@ NU5128 SuperNodes - 1.6.1 + 1.7.0 Supercharge your Godot nodes with lifecycle-aware mixins, third party source generators, script introspection, and dynamic property manipulation — all without runtime reflection! © 2023 Chickensoft Games Chickensoft diff --git a/SuperNodes/src/PowerUpsFeature/PowerUpGenerator.cs b/SuperNodes/src/PowerUpsFeature/PowerUpGenerator.cs index 56988c1..c22112a 100644 --- a/SuperNodes/src/PowerUpsFeature/PowerUpGenerator.cs +++ b/SuperNodes/src/PowerUpsFeature/PowerUpGenerator.cs @@ -14,12 +14,14 @@ public interface IPowerUpGenerator { IPowerUpGeneratorService PowerUpGeneratorService { get; } /// - /// Generates an applied PowerUp implementation on a specific SuperNode. + /// Generates an applied PowerUp implementation on a specific SuperNode or + /// SuperObject. /// /// PowerUp to generate. - /// SuperNode to apply the PowerUp to. + /// SuperNode or SuperObject to apply the PowerUp to. + /// /// Generated source string. - string GeneratePowerUp(PowerUp powerUp, SuperNode node); + string GeneratePowerUp(PowerUp powerUp, SuperBase node); } public class PowerUpGenerator : ChickensoftGenerator, IPowerUpGenerator { @@ -29,7 +31,7 @@ public PowerUpGenerator(IPowerUpGeneratorService powerUpGeneratorService) { PowerUpGeneratorService = powerUpGeneratorService; } - public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { + public string GeneratePowerUp(PowerUp powerUp, SuperBase node) { // Edit the pieces of the user's power-up needed to make it suitable to be // a partial class of the specific node script it's applied to. @@ -45,13 +47,13 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { } var root = (CompilationUnitSyntax)tree.GetRoot(); - var classDeclaration = (ClassDeclarationSyntax)root.Members.First(); + var typeDeclaration = (TypeDeclarationSyntax)root.Members.First(); var interfaces = powerUp.Interfaces; // Strip [PowerUp] attribute off the class declaration - var newClassDeclaration = classDeclaration.WithAttributeLists( + var newTypeDeclaration = typeDeclaration.WithAttributeLists( SyntaxFactory.List( - classDeclaration.AttributeLists.Where( + typeDeclaration.AttributeLists.Where( attributeList => attributeList.Attributes.All( attribute => attribute.Name.ToString() != Constants.POWER_UP_ATTRIBUTE_NAME @@ -76,7 +78,7 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { // a parameterless constructor to satisfy Godot's node requirements. .WithMembers( SyntaxFactory.List( - classDeclaration.Members.Where( + typeDeclaration.Members.Where( member => member is not ConstructorDeclarationSyntax ) ) @@ -92,7 +94,7 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { // // Static inheritance is available in .NET 7. Eventually, we will support // it so PowerUps don't have to declare stubs. - var membersToRemove = newClassDeclaration.Members.Where( + var membersToRemove = newTypeDeclaration.Members.Where( member => member.AttributeLists.Any( attributeList => attributeList.Attributes.Any( attribute => attribute.Name.ToString() == Constants.POWER_UP_IGNORE_ATTRIBUTE_NAME @@ -100,13 +102,13 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { ) && member is not null ); - newClassDeclaration = newClassDeclaration.RemoveNodes( + newTypeDeclaration = newTypeDeclaration.RemoveNodes( membersToRemove, SyntaxRemoveOptions.KeepExteriorTrivia )!; if (interfaces.Length > 0) { // Add only interfaces back to the base list. - newClassDeclaration = newClassDeclaration.WithBaseList( + newTypeDeclaration = newTypeDeclaration.WithBaseList( SyntaxFactory.BaseList( SyntaxFactory.SeparatedList( interfaces.Select( @@ -125,7 +127,7 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { } // Edit the user's power up class tree based on the above changes. - root = root.ReplaceNode(classDeclaration, newClassDeclaration); + root = root.ReplaceNode(typeDeclaration, newTypeDeclaration); tree = tree.WithRootAndOptions(root, tree.Options); var powerUpRewriter = PowerUpGeneratorService.CreatePowerUpRewriter( @@ -139,7 +141,7 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { ); var allUsings = powerUp.Usings.Union( - new string[] { "Godot", "SuperNodes.Types" } + new string[] { "SuperNodes.Types" } ).Distinct(); var usings = allUsings @@ -152,12 +154,38 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { ) .Select(@using => $"using {@using};").ToImmutableArray(); + var prefixSource = ""; + var suffixSource = ""; + var containingTypes = 0; + + foreach (var containingType in node.ContainingTypes) { + containingTypes++; + // Generate nested type containers to place source code in if the + // type is nested. + var accessibilityKeywords = containingType.AccessibilityKeywords; + var typeDeclarationKeyword = containingType.TypeDeclarationKeyword; + var fullName = containingType.FullName; + + prefixSource += + Tab(containingTypes) + $"{accessibilityKeywords} " + + $"partial {typeDeclarationKeyword} {fullName} {{\n"; + + suffixSource += Tab(containingTypes) + "}\n"; + } + + prefixSource = prefixSource.TrimEnd('\n'); + // Get the modified source code of the PowerUp itself and format it. - var source = tree - .GetRoot() - .NormalizeWhitespace(" ", "\n", true) - .ToFullString() - .NormalizeLineEndings("\n").Split('\n').ToImmutableArray(); + var source = string.Join( + "\n", + tree + .GetRoot() + .NormalizeWhitespace(" ", "\n", true) + .ToFullString() + .NormalizeLineEndings("\n") + .Split('\n') + .Select(line => Tab(node.ContainingTypes.Length + 1) + line) + ); return Format($$""" #pragma warning disable @@ -168,7 +196,9 @@ public string GeneratePowerUp(PowerUp powerUp, SuperNode node) { node.Namespace is not null, $$"""namespace {{node.Namespace}} {""" )}} - {{source}} + {{prefixSource}} + {{source}} + {{suffixSource}} {{If(node.Namespace is not null, "}")}} #nullable disable #pragma warning restore diff --git a/SuperNodes/src/PowerUpsFeature/PowerUpsRepo.cs b/SuperNodes/src/PowerUpsFeature/PowerUpsRepo.cs index 20d37c3..4dad9ad 100644 --- a/SuperNodes/src/PowerUpsFeature/PowerUpsRepo.cs +++ b/SuperNodes/src/PowerUpsFeature/PowerUpsRepo.cs @@ -27,13 +27,13 @@ public interface IPowerUpsRepo { /// node candidate provided by the generation context. /// context. /// - /// PowerUp class declaration syntax node. + /// PowerUp class declaration syntax node. /// /// Named type symbol representing the class declaration /// syntax node, if any. /// A PowerUp model. PowerUp GetPowerUp( - ClassDeclarationSyntax classDeclaration, + TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol? symbol ); } @@ -55,31 +55,32 @@ public PowerUpsRepo(ICodeService codeService) { public bool IsPowerUpSyntaxCandidate( SyntaxNode node - ) => node is ClassDeclarationSyntax classDeclaration && classDeclaration - .AttributeLists - .SelectMany(list => list.Attributes) - .Any( - attribute - => attribute.Name.ToString() == Constants.POWER_UP_ATTRIBUTE_NAME - ); + ) => node is RecordDeclarationSyntax or ClassDeclarationSyntax && + node is TypeDeclarationSyntax typeDeclaration && typeDeclaration + .AttributeLists + .SelectMany(list => list.Attributes) + .Any( + attribute + => attribute.Name.ToString() == Constants.POWER_UP_ATTRIBUTE_NAME + ); public PowerUp GetPowerUp( - ClassDeclarationSyntax classDeclaration, + TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol? symbol ) { - var name = classDeclaration.Identifier.ValueText; + var name = typeDeclaration.Identifier.ValueText; var fullName = CodeService.GetNameFullyQualified(symbol, name); - var baseClass = + var baseType = CodeService.GetBaseTypeFullyQualified(symbol, Constants.BaseClass); - var typeParameters = CodeService.GetTypeParameters(classDeclaration); + var typeParameters = CodeService.GetTypeParameters(typeDeclaration); var interfaces = CodeService.GetVisibleInterfacesFullyQualified( - classDeclaration, symbol + typeDeclaration, symbol ); var @namespace = CodeService.GetContainingNamespace(symbol); var usings = CodeService.GetUsings(symbol); - var hasOnPowerUpMethod = classDeclaration.Members.Any( + var hasOnPowerUpMethod = typeDeclaration.Members.Any( member => member is MethodDeclarationSyntax method && method.Identifier.ValueText == $"On{name}" ); @@ -90,11 +91,11 @@ public PowerUp GetPowerUp( Namespace: @namespace, Name: name, FullName: fullName, - Location: classDeclaration.GetLocation(), - BaseClass: baseClass, + Location: typeDeclaration.GetLocation(), + BaseClass: baseType, TypeParameters: typeParameters, Interfaces: interfaces, - Source: classDeclaration.ToString(), + Source: typeDeclaration.ToString(), PropsAndFields: CodeService.GetPropsAndFields(members), Usings: usings, HasOnPowerUpMethod: hasOnPowerUpMethod diff --git a/SuperNodes/src/SuperNodesFeature/SuperNodeGenerator.cs b/SuperNodes/src/SuperNodesFeature/SuperNodeGenerator.cs index 7137b5c..00532fa 100644 --- a/SuperNodes/src/SuperNodesFeature/SuperNodeGenerator.cs +++ b/SuperNodes/src/SuperNodesFeature/SuperNodeGenerator.cs @@ -15,7 +15,7 @@ public interface ISuperNodeGenerator { /// and table of PowerUps. /// Generated source string. string GenerateSuperNode( - GenerationItem item + SuperNodeGenerationItem item ); /// @@ -28,7 +28,7 @@ GenerationItem item /// PowerUps to be applied to the SuperNode. /// /// Generated source string. - string GenerateSuperNodeStatic( + string GenerateStaticReflection( GenerationItem item, ImmutableArray appliedPowerUps ); } @@ -52,7 +52,7 @@ ISuperNodeGeneratorService superNodeGeneratorService } public string GenerateSuperNode( - GenerationItem item + SuperNodeGenerationItem item ) { var node = item.SuperNode; var powerUps = item.PowerUps; @@ -66,7 +66,6 @@ GenerationItem item return Format($$""" #pragma warning disable #nullable enable - using Godot; using SuperNodes.Types; {{If( @@ -101,11 +100,11 @@ public override partial void _Notification(int what) { """).Clean(); } - public string GenerateSuperNodeStatic( + public string GenerateStaticReflection( GenerationItem item, ImmutableArray appliedPowerUps ) { - var node = item.SuperNode; - var powerUpHooks = node.PowerUpHooksByFullName; + var superItem = item.SuperObj; + var powerUpHooks = superItem.PowerUpHooksByFullName; var typeParameterSubstitutions = SuperNodeGeneratorService .GetTypeParameterSubstitutions( appliedPowerUps, @@ -120,14 +119,14 @@ public string GenerateSuperNodeStatic( // Combine properties and fields from the node script and all of its // applied power-ups. - var propsAndFields = node.PropsAndFields + var propsAndFields = superItem.PropsAndFields .Concat(propsAndFieldsFromPowerUps) .OrderBy(propOrField => propOrField.Name) .ToImmutableArray(); // Combine usings from the node script and all of its applied power-ups. // (For any imported constants used in attribute constructors in the tables) - var allUsings = node.Usings + var allUsings = superItem.Usings .Concat(appliedPowerUps.SelectMany(powerUp => powerUp.Usings)) .Concat(StaticUsings) .Distinct(); @@ -146,13 +145,18 @@ public string GenerateSuperNodeStatic( .GenerateStaticPropsAndFields(propsAndFields); var getTypeFn = SuperNodeGeneratorService - .GenerateGetType(node.Name, propsAndFields); + .GenerateGetType(superItem.Name, propsAndFields); var getPropertyOrFieldFn = SuperNodeGeneratorService - .GenerateGetPropertyOrField(node.Name, propsAndFields); + .GenerateGetPropertyOrField(superItem.Name, propsAndFields); var setPropertyOrFieldFn = SuperNodeGeneratorService - .GenerateSetPropertyOrField(node.Name, propsAndFields); + .GenerateSetPropertyOrField(superItem.Name, propsAndFields); + + var typeDeclarationKeyword = superItem.IsRecord ? "record" : "class"; + var @interface = superItem is SuperNode + ? "ISuperNode" + : "ISuperObject"; return Format($$""" #pragma warning disable @@ -160,10 +164,10 @@ public string GenerateSuperNodeStatic( {{usings}} {{If( - node.Namespace is not null, - $$"""namespace {{node.Namespace}} {""" + superItem.Namespace is not null, + $$"""namespace {{superItem.Namespace}} {""" )}} - partial class {{node.Name}} : ISuperNode { + partial {{typeDeclarationKeyword}} {{superItem.Name}} : {{@interface}} { {{propsAndFieldsReflectionTable}} {{getTypeFn}} @@ -173,7 +177,7 @@ partial class {{node.Name}} : ISuperNode { {{setPropertyOrFieldFn}} } {{If( - node.Namespace is not null, + superItem.Namespace is not null, "}" )}} #nullable disable diff --git a/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs b/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs index 9f07e35..303665f 100644 --- a/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs +++ b/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs @@ -22,6 +22,14 @@ public interface ISuperNodesRepo { /// attribute. bool IsSuperNodeSyntaxCandidate(SyntaxNode node); + /// + /// Determines whether or not a syntax node is a SuperObject. + /// + /// Syntax node to check. + /// True if the syntax node is a class or record declaration with a + /// SuperObject attribute. + bool IsSuperObjectSyntaxCandidate(SyntaxNode node); + /// /// Returns a model that represents a SuperNode based on the SuperNode syntax /// node candidate provided by the generation context. @@ -35,6 +43,21 @@ SuperNode GetSuperNode( ClassDeclarationSyntax classDeclaration, INamedTypeSymbol? symbol ); + + /// + /// Returns a model that represents a SuperObject based on the SuperObject + /// syntax node candidate provided by the generation context. + /// + /// SuperObject class declaration syntax node or + /// record declaration syntax node. + /// + /// Named type symbol representing the class declaration + /// syntax node, if any. + /// A SuperNode model. + SuperObject GetSuperObject( + TypeDeclarationSyntax typeDeclaration, + INamedTypeSymbol? symbol + ); } /// @@ -63,6 +86,16 @@ public bool IsSuperNodeSyntaxCandidate(SyntaxNode node) == Constants.SUPER_NODE_ATTRIBUTE_NAME ); + public bool IsSuperObjectSyntaxCandidate(SyntaxNode node) + => node is RecordDeclarationSyntax or ClassDeclarationSyntax && + node is TypeDeclarationSyntax typeDeclaration && + typeDeclaration.AttributeLists.SelectMany( + list => list.Attributes + ).Any( + attribute => attribute.Name.ToString() + == Constants.SUPER_OBJECT_ATTRIBUTE_NAME + ); + public SuperNode GetSuperNode( ClassDeclarationSyntax classDeclaration, INamedTypeSymbol? symbol @@ -107,6 +140,8 @@ var hasOnNotificationMethodHandler var members = CodeService.GetMembers(symbol); var usings = CodeService.GetUsings(symbol); + var containingTypes = + CodeService.GetContainingTypes(symbol, classDeclaration); return new SuperNode( Namespace: @namespace, @@ -120,7 +155,48 @@ var hasOnNotificationMethodHandler HasPartialNotificationMethod: hasPartialNotificationMethod, HasOnNotificationMethodHandler: hasOnNotificationMethodHandler, PropsAndFields: CodeService.GetPropsAndFields(members), - Usings: usings + Usings: usings, + ContainingTypes: containingTypes + ); + } + + public SuperObject GetSuperObject( + TypeDeclarationSyntax typeDeclaration, + INamedTypeSymbol? symbol + ) { + var name = CodeService.GetNameWithGenerics(symbol, typeDeclaration); + var nameWithoutGenerics = CodeService.GetName(symbol, typeDeclaration); + var @namespace = CodeService.GetContainingNamespace(symbol); + var baseClasses = CodeService.GetBaseClassHierarchy(symbol); + + var superObjectAttribute = CodeService.GetAttribute( + symbol, + Constants.SUPER_OBJECT_ATTRIBUTE_NAME_FULL + ); + + var lifecycleHooksResponse = CodeService.GetLifecycleHooks( + superObjectAttribute, includeLifecycleMethodStrings: false + ); + + var members = CodeService.GetMembers(symbol); + var usings = CodeService.GetUsings(symbol); + var containingTypes = + CodeService.GetContainingTypes(symbol, typeDeclaration); + + var isRecord = typeDeclaration is RecordDeclarationSyntax; + + return new SuperObject( + Namespace: @namespace, + Name: name, + NameWithoutGenerics: nameWithoutGenerics, + Location: typeDeclaration.GetLocation(), + BaseClasses: baseClasses, + LifecycleHooks: lifecycleHooksResponse.LifecycleHooks, + PowerUpHooksByFullName: lifecycleHooksResponse.PowerUpHooksByFullName, + PropsAndFields: CodeService.GetPropsAndFields(members), + Usings: usings, + IsRecord: isRecord, + ContainingTypes: containingTypes ); } } diff --git a/SuperNodes/src/SuperNodesGenerator.cs b/SuperNodes/src/SuperNodesGenerator.cs index 59faa11..1e77cde 100644 --- a/SuperNodes/src/SuperNodesGenerator.cs +++ b/SuperNodes/src/SuperNodesGenerator.cs @@ -81,14 +81,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { } ); + var superObjectCandidates = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (SyntaxNode node, CancellationToken _) + => SuperNodesRepo.IsSuperObjectSyntaxCandidate(node), + transform: (GeneratorSyntaxContext context, CancellationToken _) => { + var typeDeclaration = (TypeDeclarationSyntax)context.Node; + return SuperNodesRepo.GetSuperObject( + typeDeclaration, + context.SemanticModel.GetDeclaredSymbol(typeDeclaration) + ); + } + ); + var powerUpCandidates = context.SyntaxProvider.CreateSyntaxProvider( predicate: (SyntaxNode node, CancellationToken _) => PowerUpsRepo.IsPowerUpSyntaxCandidate(node), transform: (GeneratorSyntaxContext context, CancellationToken _) => { - var classDeclaration = (ClassDeclarationSyntax)context.Node; + var typeDeclaration = (TypeDeclarationSyntax)context.Node; return PowerUpsRepo.GetPowerUp( - classDeclaration, - context.SemanticModel.GetDeclaredSymbol(classDeclaration) + typeDeclaration, + context.SemanticModel.GetDeclaredSymbol(typeDeclaration) ); } ); @@ -100,7 +112,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { // among other things), but it allows for performance (supposedly, if // you use cache-friendly values). I'm not really sure how cache-friendly // this generator is yet, but we'll get there. - var generationItems = superNodeCandidates + var superNodeGenerationItems = superNodeCandidates .Combine( powerUpCandidates.Collect().Select( (s, _) => s.ToImmutableDictionary( @@ -109,14 +121,36 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { ) ) ).Select( - (item, _) => new GenerationItem( + (item, _) => new SuperNodeGenerationItem( SuperNode: item.Left, PowerUps: item.Right ) ); + // Also combines each super object candidate with the list of power ups and + // the compilation. + var superObjectGenerationItems = superObjectCandidates + .Combine( + powerUpCandidates.Collect().Select( + (s, _) => s.ToImmutableDictionary( + keySelector: (powerUp) => powerUp.FullName, + elementSelector: (powerUp) => powerUp + ) + ) + ).Select( + (item, _) => new SuperObjectGenerationItem( + SuperObject: item.Left, + PowerUps: item.Right + ) + ); + context.RegisterSourceOutput( - source: generationItems, + source: superNodeGenerationItems, + action: Execute + ); + + context.RegisterSourceOutput( + source: superObjectGenerationItems, action: Execute ); @@ -146,41 +180,48 @@ public void Execute( SourceProductionContext context, GenerationItem item ) { - var superNode = item.SuperNode; - - if (!superNode.HasPartialNotificationMethod) { - context.ReportDiagnostic( - Diagnostic.Create( - new DiagnosticDescriptor( - id: Constants.SUPER_NODE_MISSING_NOTIFICATION_METHOD, - title: "Missing partial `_Notification` method signature.", - messageFormat: "The SuperNode '{0}' is missing a partial " + - "signature for `_Notification(int what)`. Please add the " + - "following method signature to your class: " + - "`public override partial void _Notification(int what);`", - category: "SuperNode", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ), - location: superNode.Location, - superNode.Name + var superItem = item.SuperObj; + + if (item is SuperNodeGenerationItem superNodeGenerationItem) { + // Generate the stuff for a SuperNode that overrides _Notification and + // allows PowerUps to "hook" into Godot notifications. + + var superNode = superNodeGenerationItem.SuperNode; + + if (!superNode.HasPartialNotificationMethod) { + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + id: Constants.SUPER_NODE_MISSING_NOTIFICATION_METHOD, + title: "Missing partial `_Notification` method signature.", + messageFormat: "The SuperNode '{0}' is missing a partial " + + "signature for `_Notification(int what)`. Please add the " + + "following method signature to your class: " + + "`public override partial void _Notification(int what);`", + category: "SuperNode", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ), + location: superItem.Location, + superItem.Name + ) + ); + } + + context.AddSource( + $"{superItem.FilenamePrefix}.g.cs", + SourceText.From( + SuperNodeGenerator.GenerateSuperNode(superNodeGenerationItem), + Encoding.UTF8 ) ); } - context.AddSource( - $"{superNode.FilenamePrefix}.g.cs", - SourceText.From( - SuperNodeGenerator.GenerateSuperNode(item), - Encoding.UTF8 - ) - ); - var powerUps = item.PowerUps; var appliedPowerUps = new List(); // See if the node has any power-ups. - foreach (var lifecycleHook in superNode.LifecycleHooks) { + foreach (var lifecycleHook in superItem.LifecycleHooks) { if ( lifecycleHook is not PowerUpHook powerUpHook || !item.PowerUps.ContainsKey(powerUpHook.FullName) @@ -194,7 +235,7 @@ lifecycleHook is not PowerUpHook powerUpHook || // make sure the node's base class hierarchy includes the power-up's // base class - var canApplyPowerUp = superNode.BaseClasses.Contains(powerUp.BaseClass); + var canApplyPowerUp = superItem.BaseClasses.Contains(powerUp.BaseClass); if (!canApplyPowerUp) { // Log a source generator error so the user knows they can't apply @@ -211,9 +252,9 @@ lifecycleHook is not PowerUpHook powerUpHook || defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true ), - location: superNode.Location, + location: superItem.Location, powerUp.Name, - superNode.Name, + superItem.Name, powerUp.BaseClass ) ); @@ -223,18 +264,18 @@ lifecycleHook is not PowerUpHook powerUpHook || appliedPowerUps.Add(powerUp); context.AddSource( - $"{superNode.FilenamePrefix}_{powerUp.Name}.g.cs", + $"{superItem.FilenamePrefix}_{powerUp.Name}.g.cs", SourceText.From( - PowerUpGenerator.GeneratePowerUp(powerUp, superNode), + PowerUpGenerator.GeneratePowerUp(powerUp, superItem), Encoding.UTF8 ) ); } context.AddSource( - $"{superNode.FilenamePrefix}_Reflection.g.cs", + $"{superItem.FilenamePrefix}_Reflection.g.cs", SourceText.From( - SuperNodeGenerator.GenerateSuperNodeStatic( + SuperNodeGenerator.GenerateStaticReflection( item, appliedPowerUps.ToImmutableArray() ), Encoding.UTF8 diff --git a/SuperNodes/src/common/models/ContainingType.cs b/SuperNodes/src/common/models/ContainingType.cs new file mode 100644 index 0000000..4afd425 --- /dev/null +++ b/SuperNodes/src/common/models/ContainingType.cs @@ -0,0 +1,42 @@ +namespace SuperNodes.Common.Models; + +using System; +using Microsoft.CodeAnalysis; + +public enum ContainingTypeKind { + Record, + Class, +} + +public record ContainingType( + string FullName, + ContainingTypeKind Kind, + Accessibility Accessibility, + bool IsPartial +) { + public string TypeDeclarationKeyword => GetTypeDeclarationKeyword(Kind); + + public string AccessibilityKeywords => + GetAccessibilityKeywords(Accessibility); + + public static string GetTypeDeclarationKeyword( + ContainingTypeKind kind + ) => kind switch { + ContainingTypeKind.Record => "record", + ContainingTypeKind.Class => "class", + _ => throw new ArgumentException($"Unknown ContainingTypeKind: {kind}"), + }; + + public static string GetAccessibilityKeywords( + Accessibility accessibility + ) => accessibility switch { + Accessibility.Public => "public", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.ProtectedOrInternal => "protected internal", + Accessibility.Private => "private", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.NotApplicable or _ => + throw new ArgumentException($"Unknown Accessibility: {accessibility}"), + }; +} diff --git a/SuperNodes/src/common/models/GenerationItem.cs b/SuperNodes/src/common/models/GenerationItem.cs index e94f44e..0dd8ac6 100644 --- a/SuperNodes/src/common/models/GenerationItem.cs +++ b/SuperNodes/src/common/models/GenerationItem.cs @@ -3,16 +3,47 @@ namespace SuperNodes.Common.Models; using System.Collections.Immutable; /// -/// Represents an item given to the generator's Execute method. Because of the -/// way incremental generator pipelines work, this is used to represent the -/// information that is needed to generate the implementation of a single -/// SuperNode. +/// Represents a SuperNode/Object item given to the generator's Execute method. +/// Because of the way incremental generator pipelines work, this is used to +/// represent the information that is needed to generate the implementation of +/// a single SuperNode/Object. /// -/// The SuperNode description to be generated. +/// The SuperNode/Object description to be generated. +/// /// A dictionary of PowerUps, keyed by the PowerUp's /// fully resolved name (e.g., global::SomeNamespace.SomePowerUp). /// public record GenerationItem( + SuperBase SuperObj, ImmutableDictionary PowerUps +); + +/// +/// Represents a SuperNode item given to the generator's Execute method. +/// Because of the way incremental generator pipelines work, this is used to +/// represent the information that is needed to generate the implementation of +/// a single SuperNode. +/// +/// The SuperNode description to be generated. +/// A dictionary of PowerUps, keyed by the PowerUp's +/// fully resolved name (e.g., global::SomeNamespace.SomePowerUp). +/// +public record SuperNodeGenerationItem( SuperNode SuperNode, ImmutableDictionary PowerUps -); +) : GenerationItem(SuperNode, PowerUps); + +/// +/// Represents a SuperObject item given to the generator's Execute method. +/// Because of the way incremental generator pipelines work, this is used to +/// represent the information that is needed to generate the implementation of +/// a single SuperObject. +/// +/// The SuperObject description to be generated. +/// +/// A dictionary of PowerUps, keyed by the PowerUp's +/// fully resolved name (e.g., global::SomeNamespace.SomePowerUp). +/// +public record SuperObjectGenerationItem( + SuperObject SuperObject, + ImmutableDictionary PowerUps +) : GenerationItem(SuperObject, PowerUps); diff --git a/SuperNodes/src/common/models/SuperNode.cs b/SuperNodes/src/common/models/SuperNode.cs index d6ad921..41e1ca4 100644 --- a/SuperNodes/src/common/models/SuperNode.cs +++ b/SuperNodes/src/common/models/SuperNode.cs @@ -3,6 +3,60 @@ namespace SuperNodes.Common.Models; using System.Collections.Immutable; using Microsoft.CodeAnalysis; +/// Super object/node base class. +/// Fully qualified namespace containing the object +/// (without the global:: prefix). +/// Name of the object (not fully qualified, but includes +/// generic parameter syntax). Combine +/// with to determine the fully resolved name. +/// +/// Name of the object, without any +/// generic parameter syntax. +/// The location of the class declaration syntax object +/// that corresponds to the super object. +/// Array of fully qualified base types (base class +/// and implemented interfaces). +/// Lifecycle hooks that need to be invoked from +/// the SuperObject's generated _Notification(int what) method. +/// Lifecycle hooks can either be the string names of declared methods to invoke +/// (as specified in the SuperObject attribute) or a PowerUp to apply to the +/// SuperObject. +/// Applied PowerUps (as specified in the +/// SuperNode attribute), keyed by the fully resolved name of the PowerUp. +/// +/// Information about properties and fields +/// declared in the object. We track these fields so that static reflection +/// tables can be generated at build time, allowing scripts and PowerUps to +/// introspect their properties and fields (and the attributes applied to them) +/// without having to use reflection. +/// All of the using imports defined for the object. +/// +/// True if the type is a record, false if it is a class. +/// +/// Containing types (if this is a nested type). +/// +public abstract record SuperBase( + string? Namespace, + string Name, + string NameWithoutGenerics, + Location Location, + ImmutableArray BaseClasses, + ImmutableArray LifecycleHooks, + ImmutableDictionary PowerUpHooksByFullName, + ImmutableArray PropsAndFields, + IImmutableSet Usings, + bool IsRecord, + ImmutableArray ContainingTypes +) { + /// + /// Filename prefix to use when generating the SuperNode's related + /// implementation files. + /// + public string FilenamePrefix => Namespace is not "" + ? $"{Namespace}.{NameWithoutGenerics}" + : NameWithoutGenerics; +} + /// /// Represents a SuperNode. A SuperNode is a Godot node script class defined by /// a game developer. SuperNodes generate static tables containing information @@ -50,6 +104,8 @@ namespace SuperNodes.Common.Models; /// without having to use reflection. /// All of the using imports defined for the SuperNode /// script. +/// Containing types (if this is a nested type). +/// public record SuperNode( string? Namespace, string Name, @@ -62,13 +118,84 @@ public record SuperNode( bool HasPartialNotificationMethod, bool HasOnNotificationMethodHandler, ImmutableArray PropsAndFields, - IImmutableSet Usings -) { - /// - /// Filename prefix to use when generating the SuperNode's related - /// implementation files. - /// - public string FilenamePrefix => Namespace is not "" - ? $"{Namespace}.{NameWithoutGenerics}" - : NameWithoutGenerics; -} + IImmutableSet Usings, + ImmutableArray ContainingTypes +) : SuperBase( + Namespace, + Name, + NameWithoutGenerics, + Location, + BaseClasses, + LifecycleHooks, + PowerUpHooksByFullName, + PropsAndFields, + Usings, + IsRecord: false, + ContainingTypes + ); + +/// +/// Represents a SuperObject. SuperObjects are just plain C# classes or records, +/// as opposed to SuperNodes, which are Godot node script classes. SuperObjects +/// can leverage mixins, but since they don't have a _Notification method, any +/// mixins they use will not be invoked — they just add code to the SuperObject. +///
+/// SuperObjects also benefit from static reflection tables, which allow them +/// and their applied PowerUps (mixins) to introspect themselves. +///
+/// Fully qualified namespace containing the node +/// (without the global:: prefix). +/// Name of the Godot Node (not fully qualified, but includes +/// generic parameter syntax). Combine +/// with to determine the fully resolved name. +/// +/// Name of the Godot node, without any +/// generic parameter syntax. +/// The location of the class declaration syntax node +/// that corresponds to the SuperObject. +/// Array of fully qualified base types (base class +/// and implemented interfaces). +/// Lifecycle hooks that need to be invoked from +/// the SuperObject's generated _Notification(int what) method. +/// Lifecycle hooks can either be the string names of declared methods to invoke +/// (as specified in the SuperObject attribute) or a PowerUp to apply to the +/// SuperObject. +/// Applied PowerUps (as specified in the +/// SuperObject attribute), keyed by the fully resolved name of the PowerUp. +/// +/// Information about properties and fields +/// declared in the SuperObject. We track these fields so that static reflection +/// tables can be generated at build time, allowing scripts and PowerUps to +/// introspect their properties and fields (and the attributes applied to them) +/// without having to use reflection. +/// All of the using imports defined for the SuperObject +/// script. +/// True if the type is a record, false if it is a class. +/// +/// Containing types (if this is a nested type). +/// +public record SuperObject( + string? Namespace, + string Name, + string NameWithoutGenerics, + Location Location, + ImmutableArray BaseClasses, + ImmutableArray LifecycleHooks, + ImmutableDictionary PowerUpHooksByFullName, + ImmutableArray PropsAndFields, + IImmutableSet Usings, + bool IsRecord, + ImmutableArray ContainingTypes +) : SuperBase( + Namespace, + Name, + NameWithoutGenerics, + Location, + BaseClasses, + LifecycleHooks, + PowerUpHooksByFullName, + PropsAndFields, + Usings, + IsRecord, + ContainingTypes + ); diff --git a/SuperNodes/src/common/services/CodeService.cs b/SuperNodes/src/common/services/CodeService.cs index b3db672..0ab62e5 100644 --- a/SuperNodes/src/common/services/CodeService.cs +++ b/SuperNodes/src/common/services/CodeService.cs @@ -18,24 +18,24 @@ public interface ICodeService { /// Determines the list of visible type parameters shown on a class /// declaration syntax node. ///
- /// Class declaration syntax node. + /// Class declaration syntax node. /// Visible list of type parameters shown on that particular class /// declaration syntax node. ImmutableArray GetTypeParameters( - ClassDeclarationSyntax classDeclaration + TypeDeclarationSyntax typeDeclaration ); /// /// Determines the list of visible interfaces shown on a class declaration /// syntax node and returns the set of the fully qualified interface names. /// - /// Class declaration syntax node. + /// Class declaration syntax node. /// Named type symbol corresponding to the class /// /// Visible list of interfaces shown on that particular class /// declaration syntax node. ImmutableArray GetVisibleInterfacesFullyQualified( - ClassDeclarationSyntax classDeclaration, + TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol? symbol ); @@ -43,11 +43,11 @@ ImmutableArray GetVisibleInterfacesFullyQualified( /// Determines the list of visible generic interfaces shown on a class /// declaration syntax node. ///
- /// Class declaration syntax node. + /// Class declaration syntax node. /// Visible list of generic interfaces shown on that particular class /// declaration syntax node. ImmutableArray GetVisibleGenericInterfaces( - ClassDeclarationSyntax classDeclaration + TypeDeclarationSyntax typeDeclaration ); /// @@ -161,9 +161,11 @@ string GetNameWithGenerics( /// /// SuperNode attribute data found on a class symbol. /// + /// True by default: set to false + /// to ignore method name strings as mixins. /// Lifecycle hook information. LifecycleHooksResponse GetLifecycleHooks( - AttributeData? attribute + AttributeData? attribute, bool includeLifecycleMethodStrings = true ); /// @@ -189,6 +191,19 @@ SyntaxList members bool HasOnNotificationMethodHandler( SyntaxList members ); + + /// + /// Inspects a potentially nested type and returns its containing types in + /// order of outermost to innermost. + /// + /// Potential nested type. + /// Fallback syntax node. + /// Array of containing types, ordered by outermost to innermost. + /// + ImmutableArray GetContainingTypes( + INamedTypeSymbol? symbol, + TypeDeclarationSyntax fallbackType + ); } /// @@ -196,11 +211,11 @@ SyntaxList members /// public class CodeService : ICodeService { public ImmutableArray GetVisibleInterfacesFullyQualified( - ClassDeclarationSyntax classDeclaration, + TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol? symbol ) { - var nonGenericInterfaces = GetVisibleInterfaces(classDeclaration); - var genericInterfaces = GetVisibleGenericInterfaces(classDeclaration); + var nonGenericInterfaces = GetVisibleInterfaces(typeDeclaration); + var genericInterfaces = GetVisibleGenericInterfaces(typeDeclaration); var visibleInterfaces = nonGenericInterfaces .Union(genericInterfaces) .ToImmutableArray(); @@ -232,17 +247,17 @@ public ImmutableArray GetVisibleInterfacesFullyQualified( } public ImmutableArray GetTypeParameters( - ClassDeclarationSyntax classDeclaration + TypeDeclarationSyntax typeDeclaration ) => ( - classDeclaration.TypeParameterList?.Parameters + typeDeclaration.TypeParameterList?.Parameters .Select(parameter => parameter.Identifier.ValueText) .ToImmutableArray() ) ?? ImmutableArray.Empty; public ImmutableArray GetVisibleInterfaces( - ClassDeclarationSyntax classDeclaration + TypeDeclarationSyntax typeDeclaration ) => ( - classDeclaration.BaseList?.Types + typeDeclaration.BaseList?.Types .Select(type => type.Type) .OfType() .Select(type => type.Identifier.ValueText) @@ -251,9 +266,9 @@ ClassDeclarationSyntax classDeclaration ) ?? ImmutableArray.Empty; public ImmutableArray GetVisibleGenericInterfaces( - ClassDeclarationSyntax classDeclaration + TypeDeclarationSyntax typeDeclaration ) => ( - classDeclaration.BaseList?.Types + typeDeclaration.BaseList?.Types .Select(type => type.Type) .OfType() .Select(type => type.Identifier.ValueText) @@ -524,7 +539,9 @@ SyntaxList members method.ParameterList.Parameters.First().Identifier.ValueText == "what" ); - public LifecycleHooksResponse GetLifecycleHooks(AttributeData? attribute) { + public LifecycleHooksResponse GetLifecycleHooks( + AttributeData? attribute, bool includeLifecycleMethodStrings = true + ) { if (attribute is null) { return LifecycleHooksResponse.Empty; } @@ -541,7 +558,7 @@ public LifecycleHooksResponse GetLifecycleHooks(AttributeData? attribute) { foreach (var constant in arg.Values) { var constantType = constant.Type; var name = GetName(constantType); - if (name == "String") { + if (name == "String" && includeLifecycleMethodStrings) { // Found a lifecycle method. This can be the name of a method // to call from another generator or a method from a PowerUp. var stringValue = (string)constant.Value!; @@ -576,4 +593,46 @@ public LifecycleHooksResponse GetLifecycleHooks(AttributeData? attribute) { powerUpHooksByFullName.ToImmutableDictionary() ); } + + public ImmutableArray GetContainingTypes( + INamedTypeSymbol? symbol, + TypeDeclarationSyntax fallbackType + ) { + if (symbol is not INamedTypeSymbol typeSymbol) { + return ImmutableArray.Empty; + } + + var containingTypes = new List(); + var containingType = typeSymbol.ContainingType; + + while (containingType is not null) { + var kind = ContainingTypeKind.Class; + + if (containingType.IsRecord) { + kind = ContainingTypeKind.Record; + } + + var isPartial = containingType.DeclaringSyntaxReferences.Any( + syntaxRef => syntaxRef.GetSyntax() is TypeDeclarationSyntax typeDecl && + typeDecl.Modifiers.Any( + modifier => modifier.ValueText == "partial" + ) + ); + + var accessibility = containingType.DeclaredAccessibility; + + containingTypes.Add( + new ContainingType( + FullName: GetNameWithGenerics(containingType, fallbackType), + Kind: kind, + Accessibility: accessibility, + IsPartial: true + ) + ); + + containingType = containingType.ContainingType; + } + + return containingTypes.Reverse().ToImmutableArray(); + } } diff --git a/SuperNodes/src/common/utils/Constants.cs b/SuperNodes/src/common/utils/Constants.cs index f644d80..b351c77 100644 --- a/SuperNodes/src/common/utils/Constants.cs +++ b/SuperNodes/src/common/utils/Constants.cs @@ -48,20 +48,20 @@ public static readonly Dictionary ["OnEnterTree"] = new LifecycleMethod( "NotificationEnterTree", VOID, NoArgs ), - ["OnWmWindowFocusIn"] = new LifecycleMethod( - "NotificationWmWindowFocusIn", VOID, NoArgs + ["OnWMWindowFocusIn"] = new LifecycleMethod( + "NotificationWMWindowFocusIn", VOID, NoArgs ), - ["OnWmWindowFocusOut"] = new LifecycleMethod( - "NotificationWmWindowFocusOut", VOID, NoArgs + ["OnWMWindowFocusOut"] = new LifecycleMethod( + "NotificationWMWindowFocusOut", VOID, NoArgs ), - ["OnWmCloseRequest"] = new LifecycleMethod( - "NotificationWmCloseRequest", VOID, NoArgs + ["OnWMCloseRequest"] = new LifecycleMethod( + "NotificationWMCloseRequest", VOID, NoArgs ), - ["OnWmSizeChanged"] = new LifecycleMethod( - "NotificationWmSizeChanged", VOID, NoArgs + ["OnWMSizeChanged"] = new LifecycleMethod( + "NotificationWMSizeChanged", VOID, NoArgs ), - ["OnWmDpiChange"] = new LifecycleMethod( - "NotificationWmDpiChange", VOID, NoArgs + ["OnWMDpiChange"] = new LifecycleMethod( + "NotificationWMDpiChange", VOID, NoArgs ), ["OnVpMouseEnter"] = new LifecycleMethod( "NotificationVpMouseEnter", VOID, NoArgs @@ -75,8 +75,8 @@ public static readonly Dictionary ["OnTranslationChanged"] = new LifecycleMethod( "NotificationTranslationChanged", VOID, NoArgs ), - ["OnWmAbout"] = new LifecycleMethod( - "NotificationWmAbout", VOID, NoArgs + ["OnWMAbout"] = new LifecycleMethod( + "NotificationWMAbout", VOID, NoArgs ), ["OnCrash"] = new LifecycleMethod( "NotificationCrash", VOID, NoArgs @@ -99,14 +99,14 @@ public static readonly Dictionary ["OnTextServerChanged"] = new LifecycleMethod( "NotificationTextServerChanged", VOID, NoArgs ), - ["OnWmMouseExit"] = new LifecycleMethod( - "NotificationWmMouseExit", VOID, NoArgs + ["OnWMMouseExit"] = new LifecycleMethod( + "NotificationWMMouseExit", VOID, NoArgs ), - ["OnWmMouseEnter"] = new LifecycleMethod( - "NotificationWmMouseEnter", VOID, NoArgs + ["OnWMMouseEnter"] = new LifecycleMethod( + "NotificationWMMouseEnter", VOID, NoArgs ), - ["OnWmGoBackRequest"] = new LifecycleMethod( - "NotificationWmGoBackRequest", VOID, NoArgs + ["OnWMGoBackRequest"] = new LifecycleMethod( + "NotificationWMGoBackRequest", VOID, NoArgs ), ["OnEditorPreSave"] = new LifecycleMethod( "NotificationEditorPreSave", VOID, NoArgs @@ -176,6 +176,8 @@ public static readonly Dictionary public const string SUPER_NODE_ATTRIBUTE_NAME = "SuperNode"; public const string SUPER_NODE_ATTRIBUTE_NAME_FULL = "SuperNodeAttribute"; + public const string SUPER_OBJECT_ATTRIBUTE_NAME = "SuperObject"; + public const string SUPER_OBJECT_ATTRIBUTE_NAME_FULL = "SuperObjectAttribute"; public const string POWER_UP_ATTRIBUTE_NAME = "PowerUp";