mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat: 增强生成器属性参数校验与泛型约束支持,完善诊断体系
### 属性参数校验(Attribute Validation)
- AutoUiPageGenerator
- 新增 GF_AutoBehavior_004 诊断:
- 检测 AutoUiPageAttribute 参数无效情况
- 添加测试用例验证错误参数的诊断报告
- AutoRegisterExportedCollectionsGenerator
- 新增 GF_AutoExport_008 诊断:
- 检测 RegisterExportedCollectionAttribute 参数无效情况
- 改进 TryGetRegistrationAttributeArguments 方法:
- 精确报告错误位置
- 更新文档以包含新增诊断规则
### 泛型约束支持(Generic Constraints)
- AutoUiPageGenerator / AutoRegisterModuleGenerator
- 支持以下泛型约束的正确生成:
- class?
- notnull
- unmanaged
- 添加对应测试用例确保生成正确性
### 诊断体系优化(Diagnostics Improvements)
- AutoRegisterModuleGenerator
- 重构 AutoRegisterModuleDiagnostics:
- 优化诊断定义顺序,提高可读性与维护性
This commit is contained in:
parent
be928718e3
commit
62d448354c
@ -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>(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<AutoUiPageGenerator, DefaultVerifier>
|
||||
{
|
||||
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>(T owner, string key, UiLayer layer)
|
||||
where T : CanvasItem
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[AutoUiPage("MainMenu", "Page")]
|
||||
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class MainMenu<TReference, TNotNull, TUnmanaged>
|
||||
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<AutoUiPageGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<int> Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
||||
{
|
||||
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()
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<string>();
|
||||
|
||||
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 =>
|
||||
|
||||
@ -89,4 +89,15 @@ internal static class AutoRegisterExportedCollectionsDiagnostics
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// 报告 <c>RegisterExportedCollectionAttribute</c> 构造参数不满足约定,导致无法解析注册目标成员与方法名。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ namespace GFramework.Godot.SourceGenerators.Registration;
|
||||
/// 该生成器会扫描标记了 <c>AutoRegisterExportedCollectionsAttribute</c> 的 <c>partial</c> 类型,
|
||||
/// 为其中使用 <c>RegisterExportedCollectionAttribute</c> 声明的集合成员生成集中注册方法。
|
||||
/// 仅当集合可枚举、元素类型可推导、注册表成员存在且可找到兼容的实例注册方法时才会输出代码;
|
||||
/// 否则通过 <c>GF_AutoExport_001</c> 到 <c>GF_AutoExport_007</c> 以及公共 <c>ClassMustBePartial</c> 诊断显式阻止生成。
|
||||
/// 否则通过 <c>GF_AutoExport_001</c> 到 <c>GF_AutoExport_008</c> 以及公共 <c>ClassMustBePartial</c> 诊断显式阻止生成。
|
||||
/// </remarks>
|
||||
[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;
|
||||
}
|
||||
|
||||
|
||||
@ -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>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(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<TNullableRef, TNotNull, TUnmanaged>
|
||||
where TNullableRef : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
||||
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<AutoRegisterModuleGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,9 +382,20 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
||||
var constraints = new List<string>();
|
||||
|
||||
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 =>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user