diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs index 4b9f41c1..8e7d1115 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs @@ -94,4 +94,180 @@ public class AutoUiPageGeneratorTests source, ("TestApp_MainMenu.AutoUiPage.g.cs", expected)); } + + [Test] + public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoUiPageAttribute : Attribute + { + public AutoUiPageAttribute(string key) { } + } + } + + namespace Godot + { + public class Node { } + public class CanvasItem : Node { } + public class Control : CanvasItem { } + } + + namespace GFramework.Game.Abstractions.Enums + { + public enum UiLayer + { + Page + } + } + + namespace GFramework.Game.Abstractions.UI + { + public interface IUiPageBehavior { } + } + + namespace GFramework.Godot.UI + { + using GFramework.Game.Abstractions.Enums; + using GFramework.Game.Abstractions.UI; + using Godot; + + public static class UiPageBehaviorFactory + { + public static IUiPageBehavior Create(T owner, string key, UiLayer layer) + where T : CanvasItem + { + return null!; + } + } + } + + namespace TestApp + { + [{|#0:AutoUiPage("MainMenu")|}] + public partial class MainMenu : Control + { + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoBehavior_004", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments( + "AutoUiPageAttribute", + "MainMenu", + "a string key argument and a string UiLayer name argument")); + + await test.RunAsync(); + } + + [Test] + public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged() + { + const string source = """ + #nullable enable + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoUiPageAttribute : Attribute + { + public AutoUiPageAttribute(string key, string layerName) { } + } + } + + namespace Godot + { + public class Node { } + public class CanvasItem : Node { } + public class Control : CanvasItem { } + } + + namespace GFramework.Game.Abstractions.Enums + { + public enum UiLayer + { + Page + } + } + + namespace GFramework.Game.Abstractions.UI + { + public interface IUiPageBehavior { } + } + + namespace GFramework.Godot.UI + { + using GFramework.Game.Abstractions.Enums; + using GFramework.Game.Abstractions.UI; + using Godot; + + public static class UiPageBehaviorFactory + { + public static IUiPageBehavior Create(T owner, string key, UiLayer layer) + where T : CanvasItem + { + return null!; + } + } + } + + namespace TestApp + { + [AutoUiPage("MainMenu", "Page")] + public partial class MainMenu : Control + where TReference : class? + where TNotNull : notnull + where TUnmanaged : unmanaged + { + } + } + """; + + const string expected = """ + // + #nullable enable + + namespace TestApp; + + partial class MainMenu + where TReference : class? + where TNotNull : notnull + where TUnmanaged : unmanaged + { + private global::GFramework.Game.Abstractions.UI.IUiPageBehavior? __autoUiPageBehavior_Generated; + + public static string UiKeyStr => "MainMenu"; + + public global::GFramework.Game.Abstractions.UI.IUiPageBehavior GetPage() + { + return __autoUiPageBehavior_Generated ??= global::GFramework.Godot.UI.UiPageBehaviorFactory.Create(this, UiKeyStr, global::GFramework.Game.Abstractions.Enums.UiLayer.Page); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_MainMenu.AutoUiPage.g.cs", expected)); + } } diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index dd9b1c49..1c4927a4 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -377,6 +377,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests 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; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [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() { diff --git a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md index e3da075e..73b213f9 100644 --- a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -32,3 +32,4 @@ GF_AutoExport_005 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics GF_AutoExport_006 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics GF_AutoExport_007 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics + GF_AutoExport_008 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs index 18f82597..1e0fe7f6 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs @@ -138,6 +138,14 @@ public sealed class AutoUiPageGenerator : IIncrementalGenerator attribute.ConstructorArguments[0].Value is not string key || attribute.ConstructorArguments[1].Value is not string layerName) { + context.ReportDiagnostic(Diagnostic.Create( + AutoBehaviorDiagnostics.InvalidAttributeArguments, + attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() + ?? typeSymbol.Locations.FirstOrDefault() + ?? Location.None, + "AutoUiPageAttribute", + typeSymbol.Name, + "a string key argument and a string UiLayer name argument")); return false; } @@ -233,9 +241,20 @@ public sealed class AutoUiPageGenerator : IIncrementalGenerator var constraints = new List(); if (typeParameter.HasReferenceTypeConstraint) - constraints.Add("class"); + { + constraints.Add( + typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated + ? "class?" + : "class"); + } - if (typeParameter.HasValueTypeConstraint) + if (typeParameter.HasNotNullConstraint) + constraints.Add("notnull"); + + // unmanaged implies the value-type constraint and must replace struct in generated constraints. + if (typeParameter.HasUnmanagedTypeConstraint) + constraints.Add("unmanaged"); + else if (typeParameter.HasValueTypeConstraint) constraints.Add("struct"); constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint => diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs index 15430d73..d77b3255 100644 --- a/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs +++ b/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs @@ -89,4 +89,15 @@ internal static class AutoRegisterExportedCollectionsDiagnostics Category, DiagnosticSeverity.Error, true); + + /// + /// 报告 RegisterExportedCollectionAttribute 构造参数不满足约定,导致无法解析注册目标成员与方法名。 + /// + public static readonly DiagnosticDescriptor InvalidAttributeArguments = new( + "GF_AutoExport_008", + "RegisterExportedCollection attribute arguments are invalid", + "Attribute 'RegisterExportedCollectionAttribute' on member '{0}' must provide a string registry member name and a string register method name", + Category, + DiagnosticSeverity.Error, + true); } diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index bbd07644..df0c433b 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -12,7 +12,7 @@ namespace GFramework.Godot.SourceGenerators.Registration; /// 该生成器会扫描标记了 AutoRegisterExportedCollectionsAttributepartial 类型, /// 为其中使用 RegisterExportedCollectionAttribute 声明的集合成员生成集中注册方法。 /// 仅当集合可枚举、元素类型可推导、注册表成员存在且可找到兼容的实例注册方法时才会输出代码; -/// 否则通过 GF_AutoExport_001GF_AutoExport_007 以及公共 ClassMustBePartial 诊断显式阻止生成。 +/// 否则通过 GF_AutoExport_001GF_AutoExport_008 以及公共 ClassMustBePartial 诊断显式阻止生成。 /// [Generator] public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator @@ -218,7 +218,8 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - if (!TryGetRegistrationAttributeArguments(attribute, out var registryMemberName, out var registerMethodName)) + if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName, + out var registerMethodName)) return false; var registryMember = ownerType.GetMembers(registryMemberName) @@ -319,6 +320,8 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener } private static bool TryGetRegistrationAttributeArguments( + SourceProductionContext context, + ISymbol collectionMember, AttributeData attribute, out string registryMemberName, out string registerMethodName) @@ -330,6 +333,12 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener attribute.ConstructorArguments[0].Value is not string registryName || attribute.ConstructorArguments[1].Value is not string methodName) { + context.ReportDiagnostic(Diagnostic.Create( + AutoRegisterExportedCollectionsDiagnostics.InvalidAttributeArguments, + attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() + ?? collectionMember.Locations.FirstOrDefault() + ?? Location.None, + collectionMember.Name)); return false; } diff --git a/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs index 2ced1eed..1dc54318 100644 --- a/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs @@ -105,4 +105,103 @@ public class AutoRegisterModuleGeneratorTests source, ("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected)); } + + [Test] + public async Task Generates_Type_Constraints_For_NullableReference_NotNull_And_Unmanaged() + { + const string source = """ + #nullable enable + using System; + using GFramework.SourceGenerators.Abstractions.Architectures; + + namespace GFramework.SourceGenerators.Abstractions.Architectures + { + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoRegisterModuleAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + public sealed class RegisterModelAttribute : Attribute + { + public RegisterModelAttribute(Type modelType) { } + } + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + public sealed class RegisterSystemAttribute : Attribute + { + public RegisterSystemAttribute(Type systemType) { } + } + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + public sealed class RegisterUtilityAttribute : Attribute + { + public RegisterUtilityAttribute(Type utilityType) { } + } + } + + namespace GFramework.Core.Abstractions.Architectures + { + public interface IArchitecture + { + T RegisterModel(T model) where T : GFramework.Core.Abstractions.Model.IModel; + T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem; + T RegisterUtility(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility; + } + } + + namespace GFramework.Core.Abstractions.Model + { + public interface IModel { } + } + + namespace GFramework.Core.Abstractions.Systems + { + public interface ISystem { } + } + + namespace GFramework.Core.Abstractions.Utility + { + public interface IUtility { } + } + + namespace TestApp + { + using GFramework.Core.Abstractions.Model; + using GFramework.SourceGenerators.Abstractions.Architectures; + + public sealed class PlayerModel : IModel { } + + [AutoRegisterModule] + [RegisterModel(typeof(PlayerModel))] + public partial class GameplayModule + where TNullableRef : class? + where TNotNull : notnull + where TUnmanaged : unmanaged + { + } + } + """; + + const string expected = """ + // + #nullable enable + + namespace TestApp; + + partial class GameplayModule + where TNullableRef : class? + where TNotNull : notnull + where TUnmanaged : unmanaged + { + public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture) + { + architecture.RegisterModel(new global::TestApp.PlayerModel()); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected)); + } } diff --git a/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs b/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs index 7d5832a1..8c09a80a 100644 --- a/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs +++ b/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs @@ -382,9 +382,20 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator var constraints = new List(); if (typeParameter.HasReferenceTypeConstraint) - constraints.Add("class"); + { + constraints.Add( + typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated + ? "class?" + : "class"); + } - if (typeParameter.HasValueTypeConstraint) + if (typeParameter.HasNotNullConstraint) + constraints.Add("notnull"); + + // unmanaged implies the value-type constraint and must replace struct in generated constraints. + if (typeParameter.HasUnmanagedTypeConstraint) + constraints.Add("unmanaged"); + else if (typeParameter.HasValueTypeConstraint) constraints.Add("struct"); constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint => diff --git a/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs b/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs index b2a58ce3..83b9566f 100644 --- a/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs +++ b/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs @@ -22,14 +22,6 @@ internal static class AutoRegisterModuleDiagnostics DiagnosticSeverity.Error, true); - public static readonly DiagnosticDescriptor InstallMethodConflict = new( - "GF_AutoModule_005", - "Install method conflicts with generated code", - "Class '{0}' already defines 'Install(IArchitecture)', which conflicts with AutoRegisterModule generated code", - Category, - DiagnosticSeverity.Error, - true); - public static readonly DiagnosticDescriptor RegistrationTypeMustImplementExpectedInterface = new( "GF_AutoModule_003", "Registration type does not implement the expected interface", @@ -45,4 +37,12 @@ internal static class AutoRegisterModuleDiagnostics Category, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor InstallMethodConflict = new( + "GF_AutoModule_005", + "Install method conflicts with generated code", + "Class '{0}' already defines 'Install(IArchitecture)', which conflicts with AutoRegisterModule generated code", + Category, + DiagnosticSeverity.Error, + true); }