From af0cfb6df982746c7047ac23f98f38e38fdedee6 Mon Sep 17 00:00:00 2001 From: Joanna May Date: Fri, 8 Sep 2023 20:05:57 -0500 Subject: [PATCH] feat: allow generic scripts (#12) --- .editorconfig | 2 ++ .../test/test_cases/GenericScriptTest.cs | 35 +++++++++++++++++++ .../PowerUpsFeature/PowerUpGeneratorTest.cs | 1 + .../SuperNodeGeneratorTest.cs | 2 ++ .../SuperNodesFeature/SuperNodesRepoTest.cs | 2 +- .../tests/common/models/GenerationItemTest.cs | 1 + .../tests/common/models/SuperNodeTest.cs | 2 ++ .../tests/common/services/CodeServiceTest.cs | 31 ++++++++++++++++ .../Chickensoft.SuperNodes.Types.csproj | 2 +- SuperNodes/Chickensoft.SuperNodes.csproj | 2 +- .../src/SuperNodesFeature/SuperNodesRepo.cs | 4 ++- SuperNodes/src/common/models/SuperNode.cs | 11 ++++-- SuperNodes/src/common/services/CodeService.cs | 30 ++++++++++++++++ 13 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 SuperNodes.TestCases/test/test_cases/GenericScriptTest.cs diff --git a/.editorconfig b/.editorconfig index d7f25cc..9787afd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -467,3 +467,5 @@ dotnet_diagnostic.RCS1160.severity = none dotnet_diagnostic.RCS1175.severity = none # Don't make me change if statements to conditional expressions. dotnet_diagnostic.IDE0046.severity = none +# Let me use old-school constructors if I want. +dotnet_diagnostic.IDE0290.severity = none diff --git a/SuperNodes.TestCases/test/test_cases/GenericScriptTest.cs b/SuperNodes.TestCases/test/test_cases/GenericScriptTest.cs new file mode 100644 index 0000000..59b5801 --- /dev/null +++ b/SuperNodes.TestCases/test/test_cases/GenericScriptTest.cs @@ -0,0 +1,35 @@ +namespace GenericScript; + +using Chickensoft.GoDotTest; +using Godot; +using Shouldly; +using SuperNodes.Types; + +[SuperNode(typeof(MyPowerUp))] +public abstract partial class MySuperNodeBase : Node2D { + public override partial void _Notification(int what); + + public bool IsReady { get; private set; } + + public T? Item { get; set; } + + public void OnReady() => IsReady = true; +} + +public partial class MySuperNode : MySuperNodeBase { } + +[PowerUp] +public abstract partial class MyPowerUp : Node2D { + public void OnMyPowerUp(int what) { } +} + +public class GenericScriptTest : TestClass { + public GenericScriptTest(Node testScene) : base(testScene) { } + + [Test] + public void Test() { + var mySuperNode = new MySuperNode(); + mySuperNode._Notification((int)Node.NotificationReady); + mySuperNode.IsReady.ShouldBeTrue(); + } +} diff --git a/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs b/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs index 3d9d775..3dea25a 100644 --- a/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs +++ b/SuperNodes.Tests/tests/PowerUpsFeature/PowerUpGeneratorTest.cs @@ -85,6 +85,7 @@ public void OnTestPowerUp(int what) { var superNode = new SuperNode( Namespace: "Tests", Name: "TestSuperNode", + NameWithoutGenerics: "TestSuperNode", Location: new Mock().Object, BaseClasses: new string[] { "global::Godot.Node" }.ToImmutableArray(), LifecycleHooks: ImmutableArray.Empty, diff --git a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs index b692e96..b2c2634 100644 --- a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs +++ b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodeGeneratorTest.cs @@ -32,6 +32,7 @@ public void GeneratesSuperNode() { SuperNode: new SuperNode( Namespace: "global::Tests", Name: "TestSuperNode", + NameWithoutGenerics: "TestSuperNode", Location: new Mock().Object, BaseClasses: new string[] { "global::Godot.Node" }.ToImmutableArray(), LifecycleHooks: lifecycleHooks, @@ -184,6 +185,7 @@ public void GeneratesSuperNodeStaticReflectionTables() { var superNode = new SuperNode( Namespace: "global::Tests", Name: "TestSuperNode", + NameWithoutGenerics: "TestSuperNode", Location: new Mock().Object, BaseClasses: new string[] { "global::Godot.Node" }.ToImmutableArray(), LifecycleHooks: ImmutableArray.Empty, diff --git a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs index 9420697..7f86ea5 100644 --- a/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs +++ b/SuperNodes.Tests/tests/SuperNodesFeature/SuperNodesRepoTest.cs @@ -84,7 +84,7 @@ public void OnReady() { } code, out var symbol ); - codeService.Setup(cs => cs.GetName(symbol, node)) + codeService.Setup(cs => cs.GetNameWithGenerics(symbol, node)) .Returns(symbol.Name); codeService.Setup(cs => cs.GetContainingNamespace(symbol)) .Returns("Tests"); diff --git a/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs b/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs index 57ad53d..183f986 100644 --- a/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs +++ b/SuperNodes.Tests/tests/common/models/GenerationItemTest.cs @@ -13,6 +13,7 @@ public void Initializes() { var superNode = new SuperNode( Namespace: "global::Tests", Name: "SuperNode", + NameWithoutGenerics: "SuperNode", Location: new Mock().Object, BaseClasses: new string[] { "global::Godot.Node" }.ToImmutableArray(), LifecycleHooks: ImmutableArray.Empty, diff --git a/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs b/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs index 3b8bc33..863752f 100644 --- a/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs +++ b/SuperNodes.Tests/tests/common/models/SuperNodeTest.cs @@ -13,6 +13,7 @@ public void Initializes() { var superNode = new SuperNode( Namespace: "global::Tests", Name: "SuperNode", + NameWithoutGenerics: "SuperNode", Location: new Mock().Object, BaseClasses: new string[] { "global::Godot.Node" }.ToImmutableArray(), LifecycleHooks: ImmutableArray.Empty, @@ -33,6 +34,7 @@ public void FilenamePrefixHandlesEmptyNamespace() { var superNode = new SuperNode( Namespace: "", Name: "SuperNode", + NameWithoutGenerics: "SuperNode", Location: new Mock().Object, BaseClasses: new string[] { "global::Godot.Node" }.ToImmutableArray(), LifecycleHooks: ImmutableArray.Empty, diff --git a/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs b/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs index 0e7d49c..cc8170b 100644 --- a/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs +++ b/SuperNodes.Tests/tests/common/services/CodeServiceTest.cs @@ -551,6 +551,37 @@ public class TestClass { } codeService.GetName(null, node).ShouldBe(node.Identifier.ValueText); } + [Fact] + public void GetNameWithFallbackAndGenerics() { + var code = """ + namespace Tests { + public class TestClass { } + } + """; + + var node = Tester.Parse(code); + + var symbol = new Mock(); + + var ta = new Mock(); + ta.Setup(s => s.Name).Returns("TA"); + + var tb = new Mock(); + tb.Setup(s => s.Name).Returns("TB"); + + symbol.Setup(s => s.Name).Returns("TestClass"); + symbol.Setup(s => s.TypeParameters).Returns( + new[] { ta.Object, tb.Object }.ToImmutableArray() + ); + + var codeService = new CodeService(); + + codeService.GetNameWithGenerics(symbol.Object, node) + .ShouldBe("TestClass"); + codeService.GetNameWithGenerics(null, node) + .ShouldBe(node.Identifier.ValueText); + } + [Fact] public void HasOnNotificationMethodHandlerFindsHandler() { var code = """ diff --git a/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj b/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj index 502dc04..8e01065 100644 --- a/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj +++ b/SuperNodes.Types/Chickensoft.SuperNodes.Types.csproj @@ -11,7 +11,7 @@ portable SuperNodes Types - 1.5.0 + 1.5.1 Runtime types for SuperNodes. © 2023 Chickensoft Chickensoft diff --git a/SuperNodes/Chickensoft.SuperNodes.csproj b/SuperNodes/Chickensoft.SuperNodes.csproj index c99a106..686c6c8 100644 --- a/SuperNodes/Chickensoft.SuperNodes.csproj +++ b/SuperNodes/Chickensoft.SuperNodes.csproj @@ -13,7 +13,7 @@ NU5128 SuperNodes - 1.5.0 + 1.5.1 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/SuperNodesFeature/SuperNodesRepo.cs b/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs index e2ecd1a..9f07e35 100644 --- a/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs +++ b/SuperNodes/src/SuperNodesFeature/SuperNodesRepo.cs @@ -67,7 +67,8 @@ public SuperNode GetSuperNode( ClassDeclarationSyntax classDeclaration, INamedTypeSymbol? symbol ) { - var name = CodeService.GetName(symbol, classDeclaration); + var name = CodeService.GetNameWithGenerics(symbol, classDeclaration); + var nameWithoutGenerics = CodeService.GetName(symbol, classDeclaration); var @namespace = CodeService.GetContainingNamespace(symbol); var baseClasses = CodeService.GetBaseClassHierarchy(symbol); @@ -110,6 +111,7 @@ var hasOnNotificationMethodHandler return new SuperNode( Namespace: @namespace, Name: name, + NameWithoutGenerics: nameWithoutGenerics, Location: classDeclaration.GetLocation(), BaseClasses: baseClasses, LifecycleHooks: lifecycleHooksResponse.LifecycleHooks, diff --git a/SuperNodes/src/common/models/SuperNode.cs b/SuperNodes/src/common/models/SuperNode.cs index 1ecf9a8..d6ad921 100644 --- a/SuperNodes/src/common/models/SuperNode.cs +++ b/SuperNodes/src/common/models/SuperNode.cs @@ -14,9 +14,12 @@ namespace SuperNodes.Common.Models; /// /// Fully qualified namespace containing the node /// (without the global:: prefix). -/// Name of the Godot Node (not fully qualified). Combine +/// 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 SuperNode. /// Array of fully qualified base types (base class @@ -50,6 +53,7 @@ namespace SuperNodes.Common.Models; public record SuperNode( string? Namespace, string Name, + string NameWithoutGenerics, Location Location, ImmutableArray BaseClasses, ImmutableArray LifecycleHooks, @@ -64,6 +68,7 @@ IImmutableSet Usings /// Filename prefix to use when generating the SuperNode's related /// implementation files. /// - public string FilenamePrefix - => Namespace is not "" ? $"{Namespace}.{Name}" : Name; + public string FilenamePrefix => Namespace is not "" + ? $"{Namespace}.{NameWithoutGenerics}" + : NameWithoutGenerics; } diff --git a/SuperNodes/src/common/services/CodeService.cs b/SuperNodes/src/common/services/CodeService.cs index 338af3e..aa6bb8a 100644 --- a/SuperNodes/src/common/services/CodeService.cs +++ b/SuperNodes/src/common/services/CodeService.cs @@ -125,6 +125,19 @@ string GetName( INamedTypeSymbol? symbol, TypeDeclarationSyntax fallbackType ); + /// + /// Returns the name of the symbol, or the name of the identifier for the + /// given type declaration syntax node if the symbol is null. + ///
+ /// If the type has generics, this appends the generic parameter syntax + /// to the name. + ///
+ /// Named type symbol. + /// Fallback type declaration syntax node. + string GetNameWithGenerics( + INamedTypeSymbol? symbol, TypeDeclarationSyntax fallbackType + ); + /// /// Gets the name of the symbol, or null if the symbol is null. /// @@ -458,6 +471,23 @@ public string GetName( INamedTypeSymbol? symbol, TypeDeclarationSyntax fallbackType ) => symbol?.Name ?? fallbackType.Identifier.ValueText; + public string GetNameWithGenerics( + INamedTypeSymbol? symbol, TypeDeclarationSyntax fallbackType + ) { + var name = GetName(symbol, fallbackType); + + if (symbol?.TypeParameters.Length > 0) { + name += "<" + string.Join( + ", ", + symbol.TypeParameters.Select( + typeParameter => typeParameter.Name + ) + ) + ">"; + } + + return name; + } + public string? GetName(ISymbol? symbol) => symbol?.Name; public ImmutableArray GetMembers(INamedTypeSymbol? symbol)