using GFramework.Godot.SourceGenerators.Registration; using GFramework.Godot.SourceGenerators.Tests.Core; namespace GFramework.Godot.SourceGenerators.Tests.Registration; [TestFixture] public class AutoRegisterExportedCollectionsGeneratorTests { private const string StandardAttributeDeclarations = """ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } } """; private const string MultiDeclarationAttributeDeclarations = """ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } } """; [Test] public async Task Generates_Batch_Registration_Method_For_Annotated_Collections() { string source = CreateSource( """ public sealed class IntRegistry { public void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper where TReference : class? where TNotNull : notnull where TValue : struct where TUnmanaged : unmanaged { private readonly IntRegistry? _registry = new(); [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public List? Values { get; } = new(); } """, nullableEnabled: true); const string expected = """ // #nullable enable namespace TestApp; partial class Bootstrapper where TReference : class? where TNotNull : notnull where TValue : struct where TUnmanaged : unmanaged { private void __RegisterExportedCollections_Generated() { if (this.Values is not null && this._registry is not null) { foreach (var __generatedItem in this.Values) { this._registry.Register(__generatedItem); } } } } """; await GeneratorTest.RunAsync( source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Collection_Element_Type_Cannot_Be_Inferred() { const string source = """ using System; using System.Collections; using GFramework.Godot.SourceGenerators.Abstractions.UI; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } } } namespace TestApp { public sealed class IntRegistry { public void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly IntRegistry _registry = new(); [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public IEnumerable {|#0:Values|} { get; } = new ArrayList(); } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_005", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("Values")); await test.RunAsync(); } [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter() { string source = CreateSource( """ public sealed class ArrayRegistry { public void Register(int[] value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly ArrayRegistry _registry = new(); [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))] public List Values { get; } = new(); } """, nullableEnabled: true); const string expected = """ // #nullable enable namespace TestApp; partial class Bootstrapper { private void __RegisterExportedCollections_Generated() { if (this.Values is not null && this._registry is not null) { foreach (var __generatedItem in this.Values) { this._registry.Register(__generatedItem); } } } } """; await GeneratorTest.RunAsync( source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface() { string source = CreateSource( """ public interface IKeyValue { } public interface IRegistry { void Registry(IKeyValue mapping); } public interface IAssetRegistry : IRegistry { } public sealed class IntConfig : IKeyValue { } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly IAssetRegistry? _registry = null; [RegisterExportedCollection(nameof(_registry), "Registry")] public List? Values { get; } = new(); } """, nullableEnabled: true); const string expected = """ // #nullable enable namespace TestApp; partial class Bootstrapper { private void __RegisterExportedCollections_Generated() { if (this.Values is not null && this._registry is not null) { foreach (var __generatedItem in this.Values) { this._registry.Registry(__generatedItem); } } } } """; await GeneratorTest.RunAsync( source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Register_Method_Is_Only_Explicitly_Implemented_Interface_Member() { const string source = """ using System; using System.Collections.Generic; using GFramework.Godot.SourceGenerators.Abstractions.UI; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } } } namespace TestApp { public interface IRegistry { void Register(int value); } public sealed class ExplicitRegistry : IRegistry { void IRegistry.Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly ExplicitRegistry _registry = new(); [RegisterExportedCollection(nameof(_registry), "Register")] public List {|#0:Values|} { get; } = new(); } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("Register", "_registry", "Values")); await test.RunAsync(); } [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class() { string source = CreateSource( """ public class BaseRegistry { public void Register(int value) { } } public sealed class DerivedRegistry : BaseRegistry { } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly DerivedRegistry? _registry = new(); [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] public List? Values { get; } = new(); } """, nullableEnabled: true); const string expected = """ // #nullable enable namespace TestApp; partial class Bootstrapper { private void __RegisterExportedCollections_Generated() { if (this.Values is not null && this._registry is not null) { foreach (var __generatedItem in this.Values) { this._registry.Register(__generatedItem); } } } } """; await GeneratorTest.RunAsync( source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class() { string source = CreateSource( """ public sealed class IntRegistry { public void Register(int value) { } } public abstract class BootstrapperBase { protected readonly IntRegistry? _registry = new(); } [AutoRegisterExportedCollections] public partial class Bootstrapper : BootstrapperBase { [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public List? Values { get; } = new(); } """, nullableEnabled: true); const string expected = """ // #nullable enable namespace TestApp; partial class Bootstrapper { private void __RegisterExportedCollections_Generated() { if (this.Values is not null && this._registry is not null) { foreach (var __generatedItem in this.Values) { this._registry.Register(__generatedItem); } } } } """; await GeneratorTest.RunAsync( source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable() { string source = CreateSource( """ public sealed class IntRegistry { public void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly IntRegistry _registry = new(); [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public static List {|#0:StaticValues|} = new(); [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public static List {|#1:StaticPropertyValues|} { get; } = new(); [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public List {|#2:WriteOnlyValues|} { set { } } } """); await VerifyDiagnosticsAsync( source, skipGeneratedSourcesCheck: true, new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("StaticValues"), new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) .WithLocation(1) .WithArguments("StaticPropertyValues"), new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) .WithLocation(2) .WithArguments("WriteOnlyValues")).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Registry_Member_Is_Not_Instance_Readable() { const string source = """ using System; using System.Collections.Generic; using GFramework.Godot.SourceGenerators.Abstractions.UI; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } } } namespace TestApp { public sealed class IntRegistry { public void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private static readonly IntRegistry {|#0:_registry|} = new(); [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public List Values { get; } = new(); } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_007", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("_registry", "Values")); await test.RunAsync(); } [Test] public async Task Reports_Diagnostic_When_Register_Method_Is_Not_Accessible_From_Owner_Type() { const string source = """ using System; using System.Collections.Generic; using GFramework.Godot.SourceGenerators.Abstractions.UI; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } } } namespace TestApp { public sealed class IntRegistry { private void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly IntRegistry _registry = new(); [RegisterExportedCollection(nameof(_registry), "Register")] public List {|#0:Values|} { get; } = new(); } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("Register", "_registry", "Values")); await test.RunAsync(); } [Test] public async Task Reports_Diagnostic_When_RegisterExportedCollection_Attribute_Arguments_Are_Invalid() { const string source = """ using System; using System.Collections.Generic; using GFramework.Godot.SourceGenerators.Abstractions.UI; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class RegisterExportedCollectionAttribute : Attribute { public RegisterExportedCollectionAttribute(string registryMemberName) { } } } namespace TestApp { public sealed class IntRegistry { public void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly IntRegistry _registry = new(); [{|#0:RegisterExportedCollection(nameof(_registry))|}] public List Values { get; } = new(); } } """; var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" }, TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_008", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("Values")); await test.RunAsync(); } [Test] public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated() { string source = CreateSource( """ public sealed class IntRegistry { public void Register(int value) { } } [AutoRegisterExportedCollections] public partial class Bootstrapper { private readonly IntRegistry? _registry = new(); } [AutoRegisterExportedCollections] public partial class Bootstrapper { [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] public List? Values { get; } = new(); } """, nullableEnabled: true, allowMultipleDeclarations: true); const string expected = """ // #nullable enable namespace TestApp; partial class Bootstrapper { private void __RegisterExportedCollections_Generated() { if (this.Values is not null && this._registry is not null) { foreach (var __generatedItem in this.Values) { this._registry.Register(__generatedItem); } } } } """; await GeneratorTest.RunAsync( source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } private static string CreateSource( string applicationSource, bool nullableEnabled = false, bool allowMultipleDeclarations = false) { string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; string attributeDeclarations = allowMultipleDeclarations ? MultiDeclarationAttributeDeclarations : StandardAttributeDeclarations; return $$""" {{nullableDirective}}using System; using System.Collections; using System.Collections.Generic; using GFramework.Godot.SourceGenerators.Abstractions.UI; namespace GFramework.Godot.SourceGenerators.Abstractions.UI { {{attributeDeclarations}} } namespace TestApp { {{applicationSource}} } """; } private static Task VerifyDiagnosticsAsync( string source, bool skipGeneratedSourcesCheck = false, params DiagnosticResult[] expectedDiagnostics) { var test = new CSharpSourceGeneratorTest { TestState = { Sources = { source } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; if (skipGeneratedSourcesCheck) { test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck; } foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics) { test.ExpectedDiagnostics.Add(expectedDiagnostic); } return test.RunAsync(); } }