diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Unity.InputSystem.SourceGenerator.dll b/Packages/com.unity.inputsystem/InputSystem/Editor/Unity.InputSystem.SourceGenerator.dll new file mode 100644 index 0000000000..0055725f95 Binary files /dev/null and b/Packages/com.unity.inputsystem/InputSystem/Editor/Unity.InputSystem.SourceGenerator.dll differ diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Unity.InputSystem.SourceGenerator.dll.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Unity.InputSystem.SourceGenerator.dll.meta new file mode 100644 index 0000000000..05512adc40 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Unity.InputSystem.SourceGenerator.dll.meta @@ -0,0 +1,75 @@ +fileFormatVersion: 2 +guid: 0ef65cc9c692944d48f27bc53d62748a +labels: +- RoslynAnalyzer +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Android: + enabled: 0 + settings: + AndroidLibraryDependee: UnityLibrary + AndroidSharedLibraryType: Executable + CPU: ARMv7 + Any: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 1 + Exclude tvOS: 1 + Editor: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + Linux64: + enabled: 0 + settings: + CPU: AnyCPU + OSXUniversal: + enabled: 0 + settings: + CPU: AnyCPU + Win: + enabled: 0 + settings: + CPU: AnyCPU + Win64: + enabled: 0 + settings: + CPU: AnyCPU + WindowsStoreApps: + enabled: 0 + settings: + CPU: AnyCPU + iOS: + enabled: 0 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + tvOS: + enabled: 0 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tools/Roslyn/.config/dotnet-tools.json b/Tools/Roslyn/.config/dotnet-tools.json new file mode 100644 index 0000000000..00689077b9 --- /dev/null +++ b/Tools/Roslyn/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-reportgenerator-globaltool": { + "version": "5.5.0", + "commands": [ + "reportgenerator" + ], + "rollForward": false + } + } +} diff --git a/Tools/Roslyn/.gitignore b/Tools/Roslyn/.gitignore new file mode 100644 index 0000000000..37d1421755 --- /dev/null +++ b/Tools/Roslyn/.gitignore @@ -0,0 +1,4 @@ +*/bin +coverletlog* +TestResults +coveragereport diff --git a/Tools/Roslyn/README.md b/Tools/Roslyn/README.md new file mode 100644 index 0000000000..dee4b40b71 --- /dev/null +++ b/Tools/Roslyn/README.md @@ -0,0 +1,97 @@ +# README - Unity.InputSystem.SourceGenerator + +## Overview + +This directory contains source generators for the Unity Input System and associated automated tests. + +The source generator solution supports the following scenarios: +- Source generation for automatic type registration of custom interaction types implementing + [`UnityEngine.InputSystem.IInputInteraction`](../../Packages/com.unity.inputsystem/InputSystem/Actions/IInputInteraction.cs). +- Source generation for automatic type registration of custom processors derived from + [`UnityEngine.InputSystem.InputProcessor`](../../Packages/com.unity.inputsystem/InputSystem/Controls/InputProcessor.cs). +- Source generation for automatic type registration of custom composite bindings derived from + [`UnityEngine.InputSystem.InputBinding`](../../Packages/com.unity.inputsystem/InputSystem/Actions/InputBinding.cs). + +This allows generating required registration boilerplate code at compile-time instead of writing manual registration code. +In addition, it eliminates the need to rely on slow and memory consuming run-time operations using +[.NET Reflection API](https://learn.microsoft.com/en-us/dotnet/fundamentals/reflection/reflection). + +## How to build + +The simplest way to build the source generators are via the provided convenience scripts (internally using standard `dotnet` commands): + +To build using macOS or *nix to build both `Debug` and `Release` targets: +``` +./build.sh +``` +To build using Windows Command line prompt to build both `Debug` and `Release` targets: +``` +build +``` + +To have more control over the build or to build individual targets, inspect the above mentioned scripts which +illustrate what `dotnet` commands are used. Consult [.NET CLI tools documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet) +for additional options. + +When building the source generator, binaries are located under `./bin/`. For `Release` builds, the +resulting binary is also automatically copied into the designated folder location of the Input System package. + +## How to run tests + +To run tests and generate a test coverage report using macOS or *nix, use: +``` +./test.sh +``` +To run tests and generate a test coverage report using Windows, use: +``` +test +``` + +Note that you may have to install `.NET Runtime` or add it to path if test run fails with error and prompts you that `Microsoft.NETCore.App` is outdated or missing. + +## How to run tests with test coverage report + +In order to generate test coverage reports you need to install `reportgenerator`. + +It is restored as a global tool with: +``` +dotnet tool restore +``` + +It is installed as a global tool with: +``` +dotnet new tool-manifest # if you don't already have a .config/dotnet-tools.json +dotnet tool install dotnet-reportgenerator-globaltool +``` + +To run tests and generate a test coverage report using macOS or *nix, use: +``` +./testcov.sh +``` +To run tests and generate a test coverage report using Windows, use: +``` +testcov +``` + +## Dependencies + +The source generators themselves do not have any other dependencies than `.NET SDK` (including CLI tooling). + +Test packages have additional dependencies as follows: +- `Microsoft.NET.Test.Sdk` - Microsoft .NET test support. +- `NUnit` - NUnit testing framework. +- `NUnit.Analyzers` - Analyzer NUnit support. +- `NUnit3TestAdapter` - Adapter for running NUnit tests. +- `Microsoft.CodeAnalysis.CSharp` - Roslyn support. +- `Verify` - Diff-based verification tool that simplifies source generator testing by reviewing and accepting diffs as part of the development workflow. +- `Verify.NUnit` - NUnit adapter for `Verify` NuGet package. +- `Verify.SourceGenerators` - Source generator adapter for `Verify` NuGet package. + +All dependencies are managed via NuGet. + +## Distribution + +The resulting source generator binary is automatically copied into the Input System package. Note that the binary may +generate a diff even when nothing has changed due to timestamps within binary or due to compiler version differences. +It is recommended that the source generator binary is only commited as part of pull requests actually modifying +`Tools~/Roslyn/*`. diff --git a/Tools/Roslyn/Roslyn.sln b/Tools/Roslyn/Roslyn.sln new file mode 100644 index 0000000000..9789ae6e93 --- /dev/null +++ b/Tools/Roslyn/Roslyn.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.InputSystem.SourceGenerator", "Unity.InputSystem.SourceGenerator\Unity.InputSystem.SourceGenerator.csproj", "{CD0E7260-107F-4C7D-BC63-8B24EC8853F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.InputSystem.SourceGenerator.Tests", "Unity.InputSystem.SourceGenerator.Tests\Unity.InputSystem.SourceGenerator.Tests.csproj", "{904BB251-1F1A-46A2-B2E1-89FB7F0C4295}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{669D18C6-87D7-4FB6-8096-87F12203EAC9}" + ProjectSection(SolutionItems) = preProject + .config/dotnet-tools.json = .config/dotnet-tools.json + .gitignore = .gitignore + coverlet.runsettings = coverlet.runsettings + README.md = README.md + build.sh = build.sh + build.bat = build.bat + test.sh = test.sh + test.bat = test.bat + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.InputSystem.Mocks", "Unity.InputSystem.Mocks\Unity.InputSystem.Mocks.csproj", "{A75AFB3D-C4E9-49FF-9B0F-F37BC0F93AFE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CD0E7260-107F-4C7D-BC63-8B24EC8853F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD0E7260-107F-4C7D-BC63-8B24EC8853F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD0E7260-107F-4C7D-BC63-8B24EC8853F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD0E7260-107F-4C7D-BC63-8B24EC8853F1}.Release|Any CPU.Build.0 = Release|Any CPU + {904BB251-1F1A-46A2-B2E1-89FB7F0C4295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {904BB251-1F1A-46A2-B2E1-89FB7F0C4295}.Debug|Any CPU.Build.0 = Debug|Any CPU + {904BB251-1F1A-46A2-B2E1-89FB7F0C4295}.Release|Any CPU.ActiveCfg = Release|Any CPU + {904BB251-1F1A-46A2-B2E1-89FB7F0C4295}.Release|Any CPU.Build.0 = Release|Any CPU + {A75AFB3D-C4E9-49FF-9B0F-F37BC0F93AFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A75AFB3D-C4E9-49FF-9B0F-F37BC0F93AFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A75AFB3D-C4E9-49FF-9B0F-F37BC0F93AFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A75AFB3D-C4E9-49FF-9B0F-F37BC0F93AFE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tools/Roslyn/Unity.InputSystem.Mocks/IInputInteraction.cs b/Tools/Roslyn/Unity.InputSystem.Mocks/IInputInteraction.cs new file mode 100644 index 0000000000..c2c1cd363c --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.Mocks/IInputInteraction.cs @@ -0,0 +1,5 @@ +// ReSharper disable CheckNamespace +namespace UnityEngine.InputSystem; +// ReSharper restore CheckNamespace + +public interface IInputInteraction { } diff --git a/Tools/Roslyn/Unity.InputSystem.Mocks/InputBindingComposite.cs b/Tools/Roslyn/Unity.InputSystem.Mocks/InputBindingComposite.cs new file mode 100644 index 0000000000..245a25ccd6 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.Mocks/InputBindingComposite.cs @@ -0,0 +1,5 @@ +// ReSharper disable CheckNamespace +namespace UnityEngine.InputSystem; +// ReSharper restore CheckNamespace + +public class InputBindingComposite { } diff --git a/Tools/Roslyn/Unity.InputSystem.Mocks/InputProcessor.cs b/Tools/Roslyn/Unity.InputSystem.Mocks/InputProcessor.cs new file mode 100644 index 0000000000..e22eabfef3 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.Mocks/InputProcessor.cs @@ -0,0 +1,5 @@ +// ReSharper disable CheckNamespace +namespace UnityEngine.InputSystem; +// ReSharper restore CheckNamespace + +public class InputProcessor { } diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase#MyBindingComposite_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase#MyBindingComposite_Generated.g.verified.cs new file mode 100644 index 0000000000..6cb914cbc8 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase#MyBindingComposite_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyBindingComposite_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyBindingCompositeRegistration +{ +#if UNITY_EDITOR + static MyBindingCompositeRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterBindingComposite(typeof(global::Ns.Outer.MyBindingComposite), null); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBase#MyBindingComposite_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBase#MyBindingComposite_Generated.g.verified.cs new file mode 100644 index 0000000000..79edd88565 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBase#MyBindingComposite_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyBindingComposite_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyBindingCompositeRegistration +{ +#if UNITY_EDITOR + static MyBindingCompositeRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterBindingComposite(typeof(global::MyBindingComposite), null); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing#MyBindingComposite_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing#MyBindingComposite_Generated.g.verified.cs new file mode 100644 index 0000000000..79edd88565 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing#MyBindingComposite_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyBindingComposite_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyBindingCompositeRegistration +{ +#if UNITY_EDITOR + static MyBindingCompositeRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterBindingComposite(typeof(global::MyBindingComposite), null); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.cs new file mode 100644 index 0000000000..1b877a0fba --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputBindingCompositeTypeRegistrationTests.cs @@ -0,0 +1,33 @@ +namespace Unity.InputSystem.SourceGenerator.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] // Safe to run all tests in parallel +public class InputBindingCompositeTypeRegistrationTests +{ + private static Task Verify(string source) => TestHelper.Verify(new InputBindingCompositeRegistration(), + source, typeof(object), typeof(UnityEngine.InputSystem.InputBindingComposite)); + + [Test] public Task ShouldDoNothing_ForEmptySource() => Verify(string.Empty); + + [Test] public Task ShouldDoNothing_IfInternalClassImplementsIInputInteraction() => + Verify(@"using UnityEngine.InputSystem; class MyBindingComposite : InputBindingComposite { }"); + + [Test] public Task ShouldDoNothing_IfNestedPublicClassExtendsBaseInsideRestrictedScope() => + Verify(@"using UnityEngine.InputSystem; +class Internal +{ + public class MyBindingComposite : InputBindingComposite {} +}"); + + [Test] public Task ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing() => + Verify(@"using UnityEngine.InputSystem; public class MyBindingComposite : InputBindingComposite { }"); + + [Test] public Task ShouldGenerateRegistrationCode_IfPublicClassExtendsBase() => + Verify("public class MyBindingComposite : UnityEngine.InputSystem.InputBindingComposite { }"); + + [Test] public Task ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase() => + Verify(@"namespace Ns; +public class Outer { + public class MyBindingComposite : UnityEngine.InputSystem.InputBindingComposite { } +}"); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase#MyProcessor_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase#MyProcessor_Generated.g.verified.cs new file mode 100644 index 0000000000..b14eee2a6a --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase#MyProcessor_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyProcessor_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyProcessorRegistration +{ +#if UNITY_EDITOR + static MyProcessorRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterProcessor(typeof(global::Ns.Outer.MyProcessor)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBase#MyProcessor_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBase#MyProcessor_Generated.g.verified.cs new file mode 100644 index 0000000000..fa7a07fac4 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBase#MyProcessor_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyProcessor_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyProcessorRegistration +{ +#if UNITY_EDITOR + static MyProcessorRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterProcessor(typeof(global::MyProcessor)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing#MyProcessor_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing#MyProcessor_Generated.g.verified.cs new file mode 100644 index 0000000000..fa7a07fac4 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing#MyProcessor_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyProcessor_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyProcessorRegistration +{ +#if UNITY_EDITOR + static MyProcessorRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterProcessor(typeof(global::MyProcessor)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteraction#MyProcessor_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteraction#MyProcessor_Generated.g.verified.cs new file mode 100644 index 0000000000..fa7a07fac4 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteraction#MyProcessor_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyProcessor_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyProcessorRegistration +{ +#if UNITY_EDITOR + static MyProcessorRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterProcessor(typeof(global::MyProcessor)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteractionViaUsing#MyProcessor_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteractionViaUsing#MyProcessor_Generated.g.verified.cs new file mode 100644 index 0000000000..fa7a07fac4 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteractionViaUsing#MyProcessor_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyProcessor_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyProcessorRegistration +{ +#if UNITY_EDITOR + static MyProcessorRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterProcessor(typeof(global::MyProcessor)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.cs new file mode 100644 index 0000000000..955a283cfd --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InputProcessorTypeRegistrationTests.cs @@ -0,0 +1,33 @@ +namespace Unity.InputSystem.SourceGenerator.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] // Safe to run all tests in parallel +public class InputProcessorTypeRegistrationTests +{ + private static Task Verify(string source) => TestHelper.Verify(new InputProcessorRegistration(), + source, typeof(object), typeof(UnityEngine.InputSystem.InputProcessor)); + + [Test] public Task ShouldDoNothing_ForEmptySource() => Verify(string.Empty); + + [Test] public Task ShouldDoNothing_IfInternalClassImplementsIInputInteraction() => + Verify(@"using UnityEngine.InputSystem; class MyProcessor : InputProcessor { }"); + + [Test] public Task ShouldDoNothing_IfNestedPublicClassExtendsBaseInsideRestrictedScope() => + Verify(@"using UnityEngine.InputSystem; +class Internal +{ + public class MyProcessor : InputProcessor { } +}"); + + [Test] public Task ShouldGenerateRegistrationCode_IfPublicClassExtendsBaseViaUsing() => + Verify(@"using UnityEngine.InputSystem; public class MyProcessor : InputProcessor { }"); + + [Test] public Task ShouldGenerateRegistrationCode_IfPublicClassExtendsBase() => + Verify("public class MyProcessor : UnityEngine.InputSystem.InputProcessor { }"); + + [Test] public Task ShouldGenerateRegistrationCode_IfNestedPublicClassExtendsBase() => + Verify(@"namespace Ns; +public class Outer { + public class MyProcessor : UnityEngine.InputSystem.InputProcessor { } +}"); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassImplementsInterface#MyInteraction_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassImplementsInterface#MyInteraction_Generated.g.verified.cs new file mode 100644 index 0000000000..074c5db9d2 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfNestedPublicClassImplementsInterface#MyInteraction_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyInteraction_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyInteractionRegistration +{ +#if UNITY_EDITOR + static MyInteractionRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterInteraction(typeof(global::Ns.Outer.MyInteraction)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteraction#MyInteraction_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteraction#MyInteraction_Generated.g.verified.cs new file mode 100644 index 0000000000..3955f4ad62 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteraction#MyInteraction_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyInteraction_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyInteractionRegistration +{ +#if UNITY_EDITOR + static MyInteractionRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterInteraction(typeof(global::MyInteraction)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteractionViaUsing#MyInteraction_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteractionViaUsing#MyInteraction_Generated.g.verified.cs new file mode 100644 index 0000000000..3955f4ad62 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsIInputInteractionViaUsing#MyInteraction_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyInteraction_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyInteractionRegistration +{ +#if UNITY_EDITOR + static MyInteractionRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterInteraction(typeof(global::MyInteraction)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsInterface#MyInteraction_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsInterface#MyInteraction_Generated.g.verified.cs new file mode 100644 index 0000000000..3955f4ad62 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsInterface#MyInteraction_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyInteraction_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyInteractionRegistration +{ +#if UNITY_EDITOR + static MyInteractionRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterInteraction(typeof(global::MyInteraction)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsInterfaceViaUsing#MyInteraction_Generated.g.verified.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsInterfaceViaUsing#MyInteraction_Generated.g.verified.cs new file mode 100644 index 0000000000..3955f4ad62 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.ShouldGenerateRegistrationCode_IfPublicClassImplementsInterfaceViaUsing#MyInteraction_Generated.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: MyInteraction_Generated.g.cs +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class MyInteractionRegistration +{ +#if UNITY_EDITOR + static MyInteractionRegistration() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => InputSystem.RegisterInteraction(typeof(global::MyInteraction)); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.cs new file mode 100644 index 0000000000..d8e377c955 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/InteractionTypeRegistrationTests.cs @@ -0,0 +1,33 @@ +namespace Unity.InputSystem.SourceGenerator.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] // Safe to run all tests in parallel +public class InteractionTypeRegistrationTests +{ + private static Task Verify(string source) => TestHelper.Verify(new InputInteractionRegistration(), + source, typeof(object), typeof(UnityEngine.InputSystem.IInputInteraction)); + + [Test] public Task ShouldDoNothing_ForEmptySource() => Verify(string.Empty); + + [Test] public Task ShouldDoNothing_IfInternalClassImplementsInterface() => + Verify(@"using UnityEngine.InputSystem; class MyInteraction : IInputInteraction { }"); + + [Test] public Task ShouldDoNothing_IfNestedPublicClassImplementInterfaceInsideConstrainedScope() => + Verify(@"using UnityEngine.InputSystem; +class Internal +{ + public class MyProcessor : IInputInteraction { } +}"); + + [Test] public Task ShouldGenerateRegistrationCode_IfPublicClassImplementsInterfaceViaUsing() => + Verify(@"using UnityEngine.InputSystem; public class MyInteraction : IInputInteraction { }"); + + [Test] public Task ShouldGenerateRegistrationCode_IfPublicClassImplementsInterface() => + Verify(@"public class MyInteraction : UnityEngine.InputSystem.IInputInteraction { }"); + + [Test] public Task ShouldGenerateRegistrationCode_IfNestedPublicClassImplementsInterface() => + Verify(@"namespace Ns; +public class Outer { + public class MyInteraction : UnityEngine.InputSystem.IInputInteraction { } +}"); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/ModuleInitializer.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/ModuleInitializer.cs new file mode 100644 index 0000000000..0e98a722cb --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/ModuleInitializer.cs @@ -0,0 +1,8 @@ +namespace Unity.InputSystem.SourceGenerator.Tests; + +internal static class ModuleInitializer +{ + // Verify requires initialization. This is the recommended way. + [System.Runtime.CompilerServices.ModuleInitializer] + public static void Init() => VerifySourceGenerators.Initialize(); +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/TestHelper.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/TestHelper.cs new file mode 100644 index 0000000000..8e738a01f9 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator.Tests/TestHelper.cs @@ -0,0 +1,116 @@ +using System.Collections.Immutable; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Unity.InputSystem.SourceGenerator.Tests; + +static class TestHelper +{ + private class DefaultDiagnosticFilter + { + private readonly DiagnosticSeverity _diagnosticSeverity; + + public DefaultDiagnosticFilter(DiagnosticSeverity minSeverity = DiagnosticSeverity.Warning) + { + _diagnosticSeverity = minSeverity; + } + + public bool Accept(Diagnostic diagnostic) + { + return diagnostic.Severity >= _diagnosticSeverity && + + // We want to ignore "error CS5001: Program does not contain a static 'Main' method suitable for an entry point" + // since its expected for this scenario. + diagnostic.Id != "CS5001"; + } + } + + public static IEnumerable FilterDiagnostics(ImmutableArray diagnostics, + Predicate filter) + { + for (int i = 0; i < diagnostics.Length; ++i) + { + if (filter(diagnostics[i])) + yield return diagnostics[i]; + } + } + + public static string DiagnosticsToString(ImmutableArray diagnostics) + { + if (diagnostics == null) + return string.Empty; + if (diagnostics.Length == 0) + return string.Empty; + + var builder = new StringBuilder(); + for (int i = 0; i < diagnostics.Length; ++i) + { + if (i > 0) + builder.Append('\n'); + builder.Append(diagnostics[i]); + } + return builder.ToString(); + } + + // public static Task Verify(string source) where T : IIncrementalGenerator, new() => + // Verify(new T(), source); + + // public static Task Verify(IIncrementalGenerator generator, string source, bool checkDriverResult = true) + // { + // return Verify(generator, source, ImmutableArray.Create(), checkDriverResult); + // } + + public static Task Verify(IIncrementalGenerator generator, string source, + IEnumerable references, bool checkDriverResult = true) + { + // Configure custom compilation to include optional references + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create( + assemblyName: "Tests", + syntaxTrees: new[] { syntaxTree }, + references: references); + + // Run generators + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + driver = driver.RunGenerators(compilation); + + // Assert that compilation was successful and did not generate diagnostics. + // Note that warnings and errors related to not being a complete program may be expected. + // E.g. warning CS0649: Field 'MyInputStruct.length' is never assigned to, and will always have its default value 0. + var compilationDiagnostics = compilation.GetDiagnostics(); + if (compilationDiagnostics.Length > 0) + { + var filter = new DefaultDiagnosticFilter(); + var filteredDiagnostics = FilterDiagnostics(compilationDiagnostics, + (d) => filter.Accept(d)).ToImmutableArray(); + Assert.That(filteredDiagnostics.Length, Is.EqualTo(0), + DiagnosticsToString(filteredDiagnostics)); + } + + // Optionally check returned driver result (This may generate warnings for referenced assemblies if any) + if (checkDriverResult) + { + var result = driver.GetRunResult(); + Assert.That(result.Diagnostics.Length, Is.EqualTo(0)); + } + + // Pass the driver to Verify for output verification + return Verifier.Verify(driver); + } + + public static Task Verify(IIncrementalGenerator generator, string source, params Type[] types) + { + // Construct references to assembly locations and then utilize referenced assemblies to solve + // problem with referencing "implementation assemblies" instead of "reference assemblies". + var referencedAssemblies = Assembly.GetEntryAssembly()!.GetReferencedAssemblies(); + var references = new List(types.Length + referencedAssemblies.Length); + foreach (var type in types) + references.Add(MetadataReference.CreateFromFile(type.Assembly.Location)); + foreach (var assembly in referencedAssemblies) + references.Add(MetadataReference.CreateFromFile(Assembly.Load(assembly).Location)); + + return Verify(generator, source, references); + } +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator/Helpers.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/Helpers.cs new file mode 100644 index 0000000000..59fc4576ad --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/Helpers.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Unity.InputSystem.SourceGenerator; + +static class Helpers +{ + private static readonly HashSet ExcludedAssemblies = + [ + "Unity.InputSystem" + ]; + + public static bool IsAcceptedAssembly(INamedTypeSymbol symbol) + { + return !ExcludedAssemblies.Contains(symbol.ContainingAssembly.Identity.Name); + } + + public static bool IsEffectivelyPublic(INamedTypeSymbol type) + { + if (type.DeclaredAccessibility != Accessibility.Public) + return false; + + for (var container = type.ContainingType; + container is not null; + container = container.ContainingType) + { + if (container.DeclaredAccessibility != Accessibility.Public) + return false; + } + + return true; + } + + public static bool ImplementsInterface(INamedTypeSymbol type, INamedTypeSymbol interfaceSymbol) + => type.AllInterfaces.Contains(interfaceSymbol); + + public static bool IsOrInheritsFrom(INamedTypeSymbol type, INamedTypeSymbol baseSymbol) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current, baseSymbol)) + return true; + } + + return false; + } +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputBindingCompositeRegistration.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputBindingCompositeRegistration.cs new file mode 100644 index 0000000000..46a8111194 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputBindingCompositeRegistration.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +namespace Unity.InputSystem.SourceGenerator; + +/// +/// Source generator that registers public types derived from InputBindingComposite. +/// +[Generator] +public sealed class InputBindingCompositeRegistration() : TypeRegistrationGenerator(Base, Template, + static (symbol, baseSymbol) => Helpers.IsAcceptedAssembly(symbol) && + Helpers.IsEffectivelyPublic(symbol) && + Helpers.IsOrInheritsFrom(symbol.BaseType, baseSymbol)) +{ + private const string Base = "UnityEngine.InputSystem.InputBindingComposite"; + private const string Template = RegistrationTemplateBegin + + "InputSystem.RegisterBindingComposite(typeof(@T), null);" + + RegistrationTemplateEnd; +} diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputInteractionRegistration.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputInteractionRegistration.cs new file mode 100644 index 0000000000..e5dcbd6425 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputInteractionRegistration.cs @@ -0,0 +1,28 @@ +// Roslyn Incremental Source Generator to support automatic registration of Input System type extensions via +// in-memory generated source performing manual registration of types. +// +// Potential further improvements: +// - Assembly filtering to narrow scope. +// - For ImplementsInterface, consider type.AllInterfaces.Contains(symbol, SymbolEqualityComparer.Default); +// - Generate diagnostic warnings for types implementing interfaces with private visibility? +// - Improve generated type name generation to guarantee no clash. + +using Microsoft.CodeAnalysis; + +namespace Unity.InputSystem.SourceGenerator; + +/// +/// Source generator that registers public types implementing IInputInteraction. +/// +[Generator] +public sealed class InputInteractionRegistration() : TypeRegistrationGenerator(Interface, Template, + static (symbol, @interface) => Helpers.IsAcceptedAssembly(symbol) && + Helpers.IsEffectivelyPublic(symbol) && + Helpers.ImplementsInterface(symbol, @interface)) +{ + private const string Interface = "UnityEngine.InputSystem.IInputInteraction"; + private const string Template = RegistrationTemplateBegin + + "InputSystem.RegisterInteraction(typeof(@T));" + + RegistrationTemplateEnd; +} + diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputProcessorRegistration.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputProcessorRegistration.cs new file mode 100644 index 0000000000..b6e629da8f --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/InputProcessorRegistration.cs @@ -0,0 +1,28 @@ +// Roslyn Incremental Source Generator to support automatic registration of Input System type extensions via +// in-memory generated source performing manual registration of types. +// +// Potential further improvements: +// - Assembly filtering to narrow scope. +// - For ImplementsInterface, consider type.AllInterfaces.Contains(symbol, SymbolEqualityComparer.Default); +// - Generate diagnostic warnings for types implementing interfaces with private visibility? +// - Improve generated type name generation to guarantee no clash. + +using Microsoft.CodeAnalysis; + +namespace Unity.InputSystem.SourceGenerator; + +/// +/// Source generator that registers public types extending InputProcessor. +/// +[Generator] +public sealed class InputProcessorRegistration() : TypeRegistrationGenerator(Base, Template, + static (symbol, baseSymbol) => Helpers.IsAcceptedAssembly(symbol) && + Helpers.IsEffectivelyPublic(symbol) && + Helpers.IsOrInheritsFrom(symbol.BaseType, baseSymbol)) +{ + private const string Base = "UnityEngine.InputSystem.InputProcessor"; + private const string Template = RegistrationTemplateBegin + + "InputSystem.RegisterProcessor(typeof(@T));" + + RegistrationTemplateEnd; +} + diff --git a/Tools/Roslyn/Unity.InputSystem.SourceGenerator/TypeRegistrationGenerator.cs b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/TypeRegistrationGenerator.cs new file mode 100644 index 0000000000..4bc520f3c8 --- /dev/null +++ b/Tools/Roslyn/Unity.InputSystem.SourceGenerator/TypeRegistrationGenerator.cs @@ -0,0 +1,100 @@ +// Roslyn Incremental Source Generator to support automatic registration of Input System type extensions via +// in-memory generated source performing manual registration of types. +// +// Potential further improvements: +// - For ImplementsInterface, consider type.AllInterfaces.Contains(symbol, SymbolEqualityComparer.Default); +// - Generate diagnostic warnings for types implementing interfaces with private visibility? +// - Improve generated type name generation to guarantee no clash. + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Unity.InputSystem.SourceGenerator; + +public class TypeRegistrationGenerator : IIncrementalGenerator +{ + internal const string RegistrationTemplateBegin = @"using System; +using UnityEngine; +using UnityEngine.InputSystem; + +#if UNITY_EDITOR +[UnityEditor.InitializeOnLoad] +#endif +class @C +{ +#if UNITY_EDITOR + static @C() { Register(); } +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] + static void Register() => "; + + internal const string RegistrationTemplateEnd = @" +} +"; + + private readonly string _interface; + private readonly string _template; + private readonly System.Func _accept; + + protected TypeRegistrationGenerator(string @interface, string template, + System.Func accept) + { + _interface = @interface; + _template = template; + _accept = accept; + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Filter syntax + var typeDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax or RecordDeclarationSyntax, + static (ctx, _) => (TypeDeclarationSyntax)ctx.Node) + .Where(t => t is not null); + + // Combine syntax with compilation so we can do symbol checks + var candidateTypes = context.CompilationProvider.Combine(typeDeclarations.Collect()); + + // Finally, register source output generator + context.RegisterSourceOutput(candidateTypes, (spc, source) => + { + var (compilation, typeDecls) = source; + Process(spc, compilation, typeDecls, _interface, _template, _accept); + }); + } + + private static void Process(SourceProductionContext context, Compilation compilation, + ImmutableArray declarations, string @interface, string template, + System.Func accept) + { + var interfaceSymbol = compilation.GetTypeByMetadataName(@interface); + if (interfaceSymbol is null) + return; // symbol not in this compilation + + foreach (var decl in declarations) + { + // Get semantic model + var model = compilation.GetSemanticModel(decl.SyntaxTree); + if (model.GetDeclaredSymbol(decl) is not { } typeSymbol) + continue; + + // Skip if we shouldn't accept type + if (!accept(typeSymbol, interfaceSymbol)) + continue; + + // Generate type registration code and add to source + var fullyQualifiedTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var source = template.Replace("@C", typeSymbol.Name + "Registration") + .Replace("@T", fullyQualifiedTypeName); + + // Finally, add source to compilation context + context.AddSource($"{typeSymbol.Name}_Generated.g.cs", SourceText.From(source, Encoding.UTF8)); + } + } +} diff --git a/Tools/Roslyn/build.bat b/Tools/Roslyn/build.bat new file mode 100755 index 0000000000..948f41ccdd --- /dev/null +++ b/Tools/Roslyn/build.bat @@ -0,0 +1 @@ +dotnet build -c Debug && dotnet build -c Release diff --git a/Tools/Roslyn/build.sh b/Tools/Roslyn/build.sh new file mode 100755 index 0000000000..e4b1c439d9 --- /dev/null +++ b/Tools/Roslyn/build.sh @@ -0,0 +1 @@ +source build.bat diff --git a/Tools/Roslyn/coverlet.runsettings b/Tools/Roslyn/coverlet.runsettings new file mode 100644 index 0000000000..10433f6611 --- /dev/null +++ b/Tools/Roslyn/coverlet.runsettings @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Tools/Roslyn/test.bat b/Tools/Roslyn/test.bat new file mode 100755 index 0000000000..04162e90a2 --- /dev/null +++ b/Tools/Roslyn/test.bat @@ -0,0 +1 @@ +dotnet test diff --git a/Tools/Roslyn/test.sh b/Tools/Roslyn/test.sh new file mode 100755 index 0000000000..f00dd3f68a --- /dev/null +++ b/Tools/Roslyn/test.sh @@ -0,0 +1 @@ +source test.bat diff --git a/Tools/Roslyn/testcov.bat b/Tools/Roslyn/testcov.bat new file mode 100755 index 0000000000..2430977be9 --- /dev/null +++ b/Tools/Roslyn/testcov.bat @@ -0,0 +1 @@ +dotnet test --collect:"XPlat Code Coverage" --settings coverlet.runsettings --diag:coverletlog.txt && dotnet reportgenerator "-reports:Unity.InputSystem.SourceGenerator.Tests/TestResults/**/coverage.cobertura.xml" "-targetdir:coveragereport" -reporttypes:Html diff --git a/Tools/Roslyn/testcov.sh b/Tools/Roslyn/testcov.sh new file mode 100755 index 0000000000..1aba682f1d --- /dev/null +++ b/Tools/Roslyn/testcov.sh @@ -0,0 +1 @@ +source testcov.bat