From 78af119f38108b02e52101f4a7ce11cd89cc9c4d Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:29:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E6=84=9F=E7=9F=A5=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除ContextAwareServiceExtensions中GetService/GetSystem/GetModel/GetUtility方法的可空返回值 - 添加ContextGetGenerator源码生成器,支持通过特性自动生成上下文注入代码 - 新增GetService/GetServices/GetSystem/GetSystems/GetModel/GetModels/GetUtility/GetUtilities/GetAll特性 - 添加ContextGetDiagnostics提供注入相关的编译时诊断检查 - 实现INamedTypeSymbol扩展方法AreAllDeclarationsPartial用于检查partial类声明 - 添加ITypeSymbol扩展方法IsAssignableTo用于类型兼容性判断 - 创建FieldCandidateInfo和TypeCandidateInfo记录类型用于存储生成器候选信息 - 添加IsExternalInit内部类型支持低版本.NET框架的init-only setter功能 - 更新AnalyzerReleases.Unshipped.md添加新的诊断规则条目 - 创建完整的单元测试验证生成器功能和各种边界情况 --- .../ContextAwareServiceExtensions.cs | 8 +- .../Rule/GetAllAttribute.cs | 9 + .../Rule/GetModelAttribute.cs | 9 + .../Rule/GetModelsAttribute.cs | 9 + .../Rule/GetServiceAttribute.cs | 9 + .../Rule/GetServicesAttribute.cs | 9 + .../Rule/GetSystemAttribute.cs | 9 + .../Rule/GetSystemsAttribute.cs | 9 + .../Rule/GetUtilitiesAttribute.cs | 9 + .../Rule/GetUtilityAttribute.cs | 9 + .../Extensions/INamedTypeSymbolExtensions.cs | 70 +- .../Extensions/ITypeSymbolExtensions.cs | 39 + .../Info/FieldCandidateInfo.cs | 14 + .../Info/TypeCandidateInfo.cs | 14 + .../Internals/IsExternalInit.cs | 20 + .../Rule/ContextGetGeneratorTests.cs | 425 ++++++++++ .../AnalyzerReleases.Unshipped.md | 8 +- .../Diagnostics/ContextGetDiagnostics.cs | 75 ++ .../Internals/IsExternalInit.cs | 20 + GFramework.SourceGenerators/README.md | 46 ++ .../Rule/ContextGetGenerator.cs | 772 ++++++++++++++++++ 21 files changed, 1559 insertions(+), 33 deletions(-) create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetAllAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetModelAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetModelsAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetServiceAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetServicesAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetSystemAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetSystemsAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetUtilitiesAttribute.cs create mode 100644 GFramework.SourceGenerators.Abstractions/Rule/GetUtilityAttribute.cs create mode 100644 GFramework.SourceGenerators.Common/Extensions/ITypeSymbolExtensions.cs create mode 100644 GFramework.SourceGenerators.Common/Info/FieldCandidateInfo.cs create mode 100644 GFramework.SourceGenerators.Common/Info/TypeCandidateInfo.cs create mode 100644 GFramework.SourceGenerators.Common/Internals/IsExternalInit.cs create mode 100644 GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs create mode 100644 GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs create mode 100644 GFramework.SourceGenerators/Internals/IsExternalInit.cs create mode 100644 GFramework.SourceGenerators/README.md create mode 100644 GFramework.SourceGenerators/Rule/ContextGetGenerator.cs diff --git a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs index 9c6ed7a..e9a099d 100644 --- a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs @@ -20,7 +20,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的上下文感知对象 /// 指定类型的服务实例,如果未找到则返回 null /// 当 contextAware 参数为 null 时抛出 - public static TService? GetService(this IContextAware contextAware) where TService : class + public static TService GetService(this IContextAware contextAware) where TService : class { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); @@ -34,7 +34,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的对象 /// 指定类型的系统实例 /// 当 contextAware 为 null 时抛出 - public static TSystem? GetSystem(this IContextAware contextAware) where TSystem : class, ISystem + public static TSystem GetSystem(this IContextAware contextAware) where TSystem : class, ISystem { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); @@ -48,7 +48,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的对象 /// 指定类型的模型实例 /// 当 contextAware 为 null 时抛出 - public static TModel? GetModel(this IContextAware contextAware) where TModel : class, IModel + public static TModel GetModel(this IContextAware contextAware) where TModel : class, IModel { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); @@ -62,7 +62,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的对象 /// 指定类型的工具实例 /// 当 contextAware 为 null 时抛出 - public static TUtility? GetUtility(this IContextAware contextAware) where TUtility : class, IUtility + public static TUtility GetUtility(this IContextAware contextAware) where TUtility : class, IUtility { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetAllAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetAllAttribute.cs new file mode 100644 index 0000000..5722ff4 --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetAllAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记类需要自动推断并注入上下文相关字段。 +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class GetAllAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetModelAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetModelAttribute.cs new file mode 100644 index 0000000..31e6a8d --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetModelAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入单个模型实例。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetModelAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetModelsAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetModelsAttribute.cs new file mode 100644 index 0000000..fb14d7c --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetModelsAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入模型集合。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetModelsAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetServiceAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetServiceAttribute.cs new file mode 100644 index 0000000..5a8563f --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetServiceAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入单个服务实例。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetServiceAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetServicesAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetServicesAttribute.cs new file mode 100644 index 0000000..19cff72 --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetServicesAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入服务集合。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetServicesAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetSystemAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetSystemAttribute.cs new file mode 100644 index 0000000..c3352b2 --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetSystemAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入单个系统实例。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetSystemAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetSystemsAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetSystemsAttribute.cs new file mode 100644 index 0000000..d8b1c03 --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetSystemsAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入系统集合。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetSystemsAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetUtilitiesAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetUtilitiesAttribute.cs new file mode 100644 index 0000000..9b78d74 --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetUtilitiesAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入工具集合。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetUtilitiesAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Abstractions/Rule/GetUtilityAttribute.cs b/GFramework.SourceGenerators.Abstractions/Rule/GetUtilityAttribute.cs new file mode 100644 index 0000000..5c87bc4 --- /dev/null +++ b/GFramework.SourceGenerators.Abstractions/Rule/GetUtilityAttribute.cs @@ -0,0 +1,9 @@ +namespace GFramework.SourceGenerators.Abstractions.Rule; + +/// +/// 标记字段需要自动注入单个工具实例。 +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +public sealed class GetUtilityAttribute : Attribute +{ +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Common/Extensions/INamedTypeSymbolExtensions.cs b/GFramework.SourceGenerators.Common/Extensions/INamedTypeSymbolExtensions.cs index 681c1cf..f937981 100644 --- a/GFramework.SourceGenerators.Common/Extensions/INamedTypeSymbolExtensions.cs +++ b/GFramework.SourceGenerators.Common/Extensions/INamedTypeSymbolExtensions.cs @@ -1,5 +1,7 @@ using GFramework.SourceGenerators.Common.Info; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace GFramework.SourceGenerators.Common.Extensions; @@ -69,38 +71,50 @@ public static class INamedTypeSymbolExtensions : $"where {tp.Name} : {string.Join(", ", parts)}"; } + /// + /// 判断类型的所有声明是否均带有 partial 关键字。 + /// /// 要获取完整类名的命名类型符号 - extension(INamedTypeSymbol symbol) + /// 如果所有声明均为 partial,则返回 true + public static bool AreAllDeclarationsPartial(this INamedTypeSymbol symbol) { - /// - /// 获取命名类型符号的完整类名(包括嵌套类型名称) - /// - /// 完整的类名,格式为"外层类名.内层类名.当前类名" - public string GetFullClassName() + return symbol.DeclaringSyntaxReferences + .Select(static reference => reference.GetSyntax()) + .OfType() + .All(static declaration => + declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword))); + } + + /// + /// 获取命名类型符号的完整类名(包括嵌套类型名称) + /// + /// 要获取完整类名的命名类型符号 + /// 完整的类名,格式为"外层类名.内层类名.当前类名" + public static string GetFullClassName(this INamedTypeSymbol symbol) + { + var names = new Stack(); + var current = symbol; + + // 遍历包含类型链,将所有类型名称压入栈中 + while (current != null) { - var names = new Stack(); - var current = symbol; - - // 遍历包含类型链,将所有类型名称压入栈中 - while (current != null) - { - names.Push(current.Name); - current = current.ContainingType; - } - - // 将栈中的名称用点号连接,形成完整的类名 - return string.Join(".", names); + names.Push(current.Name); + current = current.ContainingType; } - /// - /// 获取命名类型符号的命名空间名称 - /// - /// 命名空间名称,如果是全局命名空间则返回null - public string? GetNamespace() - { - return symbol.ContainingNamespace.IsGlobalNamespace - ? null - : symbol.ContainingNamespace.ToDisplayString(); - } + // 将栈中的名称用点号连接,形成完整的类名 + return string.Join(".", names); + } + + /// + /// 获取命名类型符号的命名空间名称 + /// + /// 要获取完整类名的命名类型符号 + /// 命名空间名称,如果是全局命名空间则返回null + public static string? GetNamespace(this INamedTypeSymbol symbol) + { + return symbol.ContainingNamespace.IsGlobalNamespace + ? null + : symbol.ContainingNamespace.ToDisplayString(); } } \ No newline at end of file diff --git a/GFramework.SourceGenerators.Common/Extensions/ITypeSymbolExtensions.cs b/GFramework.SourceGenerators.Common/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 0000000..2306117 --- /dev/null +++ b/GFramework.SourceGenerators.Common/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.CodeAnalysis; + +namespace GFramework.SourceGenerators.Common.Extensions; + +/// +/// 提供 的通用符号判断扩展。 +/// +public static class ITypeSymbolExtensions +{ + /// + /// 判断当前类型是否等于或实现/继承目标类型。 + /// + /// 当前类型符号。 + /// 目标类型符号。 + /// 若等于、实现或继承则返回 true + public static bool IsAssignableTo( + this ITypeSymbol typeSymbol, + INamedTypeSymbol? targetType) + { + if (targetType is null) + return false; + + if (SymbolEqualityComparer.Default.Equals(typeSymbol, targetType)) + return true; + + if (typeSymbol is INamedTypeSymbol namedType) + { + if (namedType.AllInterfaces.Any(i => + SymbolEqualityComparer.Default.Equals(i, targetType))) + return true; + + for (var current = namedType.BaseType; current is not null; current = current.BaseType) + if (SymbolEqualityComparer.Default.Equals(current, targetType)) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Common/Info/FieldCandidateInfo.cs b/GFramework.SourceGenerators.Common/Info/FieldCandidateInfo.cs new file mode 100644 index 0000000..61174b8 --- /dev/null +++ b/GFramework.SourceGenerators.Common/Info/FieldCandidateInfo.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GFramework.SourceGenerators.Common.Info; + +/// +/// 表示字段级生成器候选成员。 +/// +/// 字段变量语法节点。 +/// 字段符号。 +public sealed record FieldCandidateInfo( + VariableDeclaratorSyntax Variable, + IFieldSymbol FieldSymbol +); \ No newline at end of file diff --git a/GFramework.SourceGenerators.Common/Info/TypeCandidateInfo.cs b/GFramework.SourceGenerators.Common/Info/TypeCandidateInfo.cs new file mode 100644 index 0000000..a3d2d76 --- /dev/null +++ b/GFramework.SourceGenerators.Common/Info/TypeCandidateInfo.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GFramework.SourceGenerators.Common.Info; + +/// +/// 表示类型级生成器候选成员。 +/// +/// 类型声明语法节点。 +/// 类型符号。 +public sealed record TypeCandidateInfo( + ClassDeclarationSyntax Declaration, + INamedTypeSymbol TypeSymbol +); \ No newline at end of file diff --git a/GFramework.SourceGenerators.Common/Internals/IsExternalInit.cs b/GFramework.SourceGenerators.Common/Internals/IsExternalInit.cs new file mode 100644 index 0000000..8a76104 --- /dev/null +++ b/GFramework.SourceGenerators.Common/Internals/IsExternalInit.cs @@ -0,0 +1,20 @@ +// IsExternalInit.cs +// This type is required to support init-only setters and record types +// when targeting netstandard2.0 or older frameworks. + +#if !NET5_0_OR_GREATER +using System.ComponentModel; + +// ReSharper disable CheckNamespace + +namespace System.Runtime.CompilerServices; + +/// +/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。 +/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ +} +#endif \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs new file mode 100644 index 0000000..ee49761 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs @@ -0,0 +1,425 @@ +using GFramework.SourceGenerators.Rule; +using GFramework.SourceGenerators.Tests.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace GFramework.SourceGenerators.Tests.Rule; + +[TestFixture] +public class ContextGetGeneratorTests +{ + [Test] + public async Task Generates_Bindings_For_ContextAwareAttribute_Class() + { + var source = """ + using System; + using System.Collections.Generic; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetServicesAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static IReadOnlyList GetServices(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface IInventoryStrategy { } + + [ContextAware] + public partial class InventoryPanel + { + [GetModel] + private IInventoryModel _model = null!; + + [GetServices] + private IReadOnlyList _strategies = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class InventoryPanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + _strategies = this.GetServices(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_InventoryPanel.ContextGet.g.cs", expected)); + Assert.Pass(); + } + + [Test] + public async Task Generates_Inferred_Bindings_For_GetAll_Class() + { + var source = """ + using System; + using System.Collections.Generic; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Abstractions.Architectures + { + public interface IArchitectureContext { } + } + + namespace GFramework.Core.Rule + { + public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static IReadOnlyList GetModels(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + public static T GetUtility(this object contextAware) => default!; + public static T GetService(this object contextAware) => default!; + } + } + + namespace Godot + { + public class Node { } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } + public interface IUiUtility : GFramework.Core.Abstractions.Utility.IUtility { } + public interface IStrategy { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private IInventoryModel _model = null!; + private IReadOnlyList _models = null!; + private ICombatSystem _system = null!; + private IUiUtility _utility = null!; + private IStrategy _service = null!; + private Godot.Node _node = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class BattlePanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + _models = this.GetModels(); + _system = this.GetSystem(); + _utility = this.GetUtility(); + _service = this.GetService(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_BattlePanel.ContextGet.g.cs", expected)); + Assert.Pass(); + } + + [Test] + public async Task Generates_Bindings_For_IContextAware_Class() + { + var source = """ + using System; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetServiceAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetService(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IStrategy { } + + public partial class StrategyHost : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetService] + private IStrategy _strategy = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class StrategyHost + { + private void __InjectContextBindings_Generated() + { + _strategy = this.GetService(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_StrategyHost.ContextGet.g.cs", expected)); + Assert.Pass(); + } + + [Test] + public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware() + { + var source = """ + using System; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel + { + [GetModel] + private IInventoryModel _model = null!; + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error) + .WithSpan(31, 30, 31, 45) + .WithArguments("InventoryPanel")); + + await test.RunAsync(); + Assert.Pass(); + } + + [Test] + public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList() + { + var source = """ + using System; + using System.Collections.Generic; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelsAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static IReadOnlyList GetModels(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModels] + private List _models = new(); + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error) + .WithSpan(40, 40, 40, 47) + .WithArguments("_models", "System.Collections.Generic.List", "GetModels")); + + await test.RunAsync(); + Assert.Pass(); + } +} \ No newline at end of file diff --git a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md index d3e0463..24f780f 100644 --- a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -7,9 +7,15 @@ -----------------------|----------------------------------|----------|------------------------ GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic + GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics + GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics + GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics + GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics + GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics + GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic - GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer \ No newline at end of file + GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer diff --git a/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs b/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs new file mode 100644 index 0000000..6735ac5 --- /dev/null +++ b/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs @@ -0,0 +1,75 @@ +using Microsoft.CodeAnalysis; + +namespace GFramework.SourceGenerators.Diagnostics; + +/// +/// 提供 Context Get 注入生成器相关诊断。 +/// +public static class ContextGetDiagnostics +{ + /// + /// 不支持在嵌套类中生成注入代码。 + /// + public static readonly DiagnosticDescriptor NestedClassNotSupported = new( + "GF_ContextGet_001", + "Context Get injection does not support nested classes", + "Class '{0}' cannot use context Get injection inside a nested type", + "GFramework.SourceGenerators.Rule", + DiagnosticSeverity.Error, + true); + + /// + /// 带注入语义的字段不能是静态字段。 + /// + public static readonly DiagnosticDescriptor StaticFieldNotSupported = new( + "GF_ContextGet_002", + "Static field is not supported for context Get injection", + "Field '{0}' cannot be static when using generated context Get injection", + "GFramework.SourceGenerators.Rule", + DiagnosticSeverity.Error, + true); + + /// + /// 带注入语义的字段不能是只读字段。 + /// + public static readonly DiagnosticDescriptor ReadOnlyFieldNotSupported = new( + "GF_ContextGet_003", + "Readonly field is not supported for context Get injection", + "Field '{0}' cannot be readonly when using generated context Get injection", + "GFramework.SourceGenerators.Rule", + DiagnosticSeverity.Error, + true); + + /// + /// 字段类型与注入特性不匹配。 + /// + public static readonly DiagnosticDescriptor InvalidBindingType = new( + "GF_ContextGet_004", + "Field type is not valid for the selected context Get attribute", + "Field '{0}' type '{1}' is not valid for [{2}]", + "GFramework.SourceGenerators.Rule", + DiagnosticSeverity.Error, + true); + + /// + /// 使用 Context Get 注入的类型必须是上下文感知类型。 + /// + public static readonly DiagnosticDescriptor ContextAwareTypeRequired = new( + "GF_ContextGet_005", + "Context-aware type is required", + "Class '{0}' must be context-aware to use generated context Get injection", + "GFramework.SourceGenerators.Rule", + DiagnosticSeverity.Error, + true); + + /// + /// 一个字段不允许同时声明多个 Context Get 特性。 + /// + public static readonly DiagnosticDescriptor MultipleBindingAttributesNotSupported = new( + "GF_ContextGet_006", + "Multiple context Get attributes are not supported on the same field", + "Field '{0}' cannot declare multiple generated context Get attributes", + "GFramework.SourceGenerators.Rule", + DiagnosticSeverity.Error, + true); +} \ No newline at end of file diff --git a/GFramework.SourceGenerators/Internals/IsExternalInit.cs b/GFramework.SourceGenerators/Internals/IsExternalInit.cs new file mode 100644 index 0000000..8a76104 --- /dev/null +++ b/GFramework.SourceGenerators/Internals/IsExternalInit.cs @@ -0,0 +1,20 @@ +// IsExternalInit.cs +// This type is required to support init-only setters and record types +// when targeting netstandard2.0 or older frameworks. + +#if !NET5_0_OR_GREATER +using System.ComponentModel; + +// ReSharper disable CheckNamespace + +namespace System.Runtime.CompilerServices; + +/// +/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。 +/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ +} +#endif \ No newline at end of file diff --git a/GFramework.SourceGenerators/README.md b/GFramework.SourceGenerators/README.md new file mode 100644 index 0000000..59e3d47 --- /dev/null +++ b/GFramework.SourceGenerators/README.md @@ -0,0 +1,46 @@ +# GFramework.SourceGenerators + +Core 侧通用源码生成器模块。 + +## Context Get 注入 + +当类本身是上下文感知类型时,可以通过字段特性生成一个手动调用的注入方法: + +- `[GetService]` +- `[GetServices]` +- `[GetSystem]` +- `[GetSystems]` +- `[GetModel]` +- `[GetModels]` +- `[GetUtility]` +- `[GetUtilities]` +- `[GetAll]` + +上下文感知类满足以下任一条件即可: + +- 类上带有 `[ContextAware]` +- 继承 `ContextAwareBase` +- 实现 `IContextAware` + +生成器会生成 `__InjectContextBindings_Generated()`,需要在合适的生命周期中手动调用。在 Godot 中通常放在 `_Ready()`: + +```csharp +using GFramework.SourceGenerators.Abstractions.Rule; + +[ContextAware] +public partial class InventoryPanel +{ + [GetModel] + private IInventoryModel _inventory = null!; + + [GetServices] + private IReadOnlyList _strategies = null!; + + public override void _Ready() + { + __InjectContextBindings_Generated(); + } +} +``` + +`[GetAll]` 作用于类本身,会自动扫描字段并推断对应的 `GetX` 调用;已显式标记字段的优先级更高。 diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs new file mode 100644 index 0000000..d2beb2a --- /dev/null +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -0,0 +1,772 @@ +using System.Collections.Immutable; +using System.Text; +using GFramework.SourceGenerators.Common.Constants; +using GFramework.SourceGenerators.Common.Diagnostics; +using GFramework.SourceGenerators.Common.Extensions; +using GFramework.SourceGenerators.Common.Info; +using GFramework.SourceGenerators.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GFramework.SourceGenerators.Rule; + +/// +/// 为上下文感知类生成 Core 上下文 Get 注入方法。 +/// +[Generator] +public sealed class ContextGetGenerator : IIncrementalGenerator +{ + private const string InjectionMethodName = "__InjectContextBindings_Generated"; + + private const string GetAllAttributeMetadataName = + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetAllAttribute"; + + private const string ContextAwareAttributeMetadataName = + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.ContextAwareAttribute"; + + private const string IContextAwareMetadataName = + $"{PathContests.CoreAbstractionsNamespace}.Rule.IContextAware"; + + private const string ContextAwareBaseMetadataName = + $"{PathContests.CoreNamespace}.Rule.ContextAwareBase"; + + private const string IModelMetadataName = + $"{PathContests.CoreAbstractionsNamespace}.Model.IModel"; + + private const string ISystemMetadataName = + $"{PathContests.CoreAbstractionsNamespace}.Systems.ISystem"; + + private const string IUtilityMetadataName = + $"{PathContests.CoreAbstractionsNamespace}.Utility.IUtility"; + + private const string IReadOnlyListMetadataName = + "System.Collections.Generic.IReadOnlyList`1"; + + private const string GodotNodeMetadataName = "Godot.Node"; + + private static readonly ImmutableArray BindingDescriptors = + [ + new( + BindingKind.Service, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetServiceAttribute", + "GetService", + false), + new( + BindingKind.Services, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetServicesAttribute", + "GetServices", + true), + new( + BindingKind.System, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetSystemAttribute", + "GetSystem", + false), + new( + BindingKind.Systems, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetSystemsAttribute", + "GetSystems", + true), + new( + BindingKind.Model, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetModelAttribute", + "GetModel", + false), + new( + BindingKind.Models, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetModelsAttribute", + "GetModels", + true), + new( + BindingKind.Utility, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetUtilityAttribute", + "GetUtility", + false), + new( + BindingKind.Utilities, + $"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetUtilitiesAttribute", + "GetUtilities", + true) + ]; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var fieldCandidates = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => IsFieldCandidate(node), + static (ctx, _) => TransformField(ctx)) + .Where(static candidate => candidate is not null) + .Collect(); + + var typeCandidates = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => IsTypeCandidate(node), + static (ctx, _) => TransformType(ctx)) + .Where(static candidate => candidate is not null) + .Collect(); + + var compilationAndFields = context.CompilationProvider.Combine(fieldCandidates); + var generationInput = compilationAndFields.Combine(typeCandidates); + + context.RegisterSourceOutput(generationInput, + static (spc, pair) => Execute( + spc, + pair.Left.Left, + pair.Left.Right, + pair.Right)); + } + + private static bool IsFieldCandidate(SyntaxNode node) + { + if (node is not VariableDeclaratorSyntax + { + Parent: VariableDeclarationSyntax + { + Parent: FieldDeclarationSyntax fieldDeclaration + } + }) + return false; + + return fieldDeclaration.AttributeLists + .SelectMany(static list => list.Attributes) + .Any(static attribute => attribute.Name.ToString().Contains("Get", StringComparison.Ordinal)); + } + + private static FieldCandidateInfo? TransformField(GeneratorSyntaxContext context) + { + if (context.Node is not VariableDeclaratorSyntax variable) + return null; + + return context.SemanticModel.GetDeclaredSymbol(variable) is IFieldSymbol fieldSymbol + ? new FieldCandidateInfo(variable, fieldSymbol) + : null; + } + + private static bool IsTypeCandidate(SyntaxNode node) + { + if (node is not ClassDeclarationSyntax classDeclaration) + return false; + + return classDeclaration.AttributeLists + .SelectMany(static list => list.Attributes) + .Any(static attribute => attribute.Name.ToString().Contains("GetAll", StringComparison.Ordinal)); + } + + private static TypeCandidateInfo? TransformType(GeneratorSyntaxContext context) + { + if (context.Node is not ClassDeclarationSyntax classDeclaration) + return null; + + return context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol typeSymbol + ? new TypeCandidateInfo(classDeclaration, typeSymbol) + : null; + } + + private static void Execute( + SourceProductionContext context, + Compilation compilation, + ImmutableArray fieldCandidates, + ImmutableArray typeCandidates) + { + if (fieldCandidates.IsDefaultOrEmpty && typeCandidates.IsDefaultOrEmpty) + return; + + var descriptors = ResolveBindingDescriptors(compilation); + var getAllAttribute = compilation.GetTypeByMetadataName(GetAllAttributeMetadataName); + + if (descriptors.Length == 0 && getAllAttribute is null) + return; + + var symbols = new ContextSymbols( + compilation.GetTypeByMetadataName(ContextAwareAttributeMetadataName), + compilation.GetTypeByMetadataName(IContextAwareMetadataName), + compilation.GetTypeByMetadataName(ContextAwareBaseMetadataName), + compilation.GetTypeByMetadataName(IModelMetadataName), + compilation.GetTypeByMetadataName(ISystemMetadataName), + compilation.GetTypeByMetadataName(IUtilityMetadataName), + compilation.GetTypeByMetadataName(IReadOnlyListMetadataName), + compilation.GetTypeByMetadataName(GodotNodeMetadataName)); + + var workItems = CollectWorkItems( + fieldCandidates, + typeCandidates, + descriptors, + getAllAttribute); + + foreach (var workItem in workItems.Values) + { + if (!CanGenerateForType(context, workItem, symbols)) + continue; + + var bindings = new List(); + var explicitFields = new HashSet(SymbolEqualityComparer.Default); + + foreach (var candidate in workItem.FieldCandidates + .OrderBy(static candidate => candidate.Variable.SpanStart) + .ThenBy(static candidate => candidate.FieldSymbol.Name, StringComparer.Ordinal)) + { + var matches = ResolveExplicitBindings(candidate.FieldSymbol, descriptors); + if (matches.Length == 0) + continue; + + explicitFields.Add(candidate.FieldSymbol); + + if (matches.Length > 1) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.MultipleBindingAttributesNotSupported, + candidate); + continue; + } + + if (!TryCreateExplicitBinding( + context, + candidate, + matches[0], + symbols, + out var binding)) + continue; + + bindings.Add(binding); + } + + if (workItem.GetAllDeclaration is not null) + { + foreach (var field in GetAllFields(workItem.TypeSymbol)) + { + if (explicitFields.Contains(field)) + continue; + + if (field.IsStatic) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.StaticFieldNotSupported, + field); + continue; + } + + if (field.IsReadOnly) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.ReadOnlyFieldNotSupported, + field); + continue; + } + + if (!TryCreateInferredBinding(field, symbols, out var binding)) + continue; + + bindings.Add(binding); + } + } + + if (bindings.Count == 0 && workItem.GetAllDeclaration is null) + continue; + + var source = GenerateSource(workItem.TypeSymbol, bindings); + context.AddSource(GetHintName(workItem.TypeSymbol), source); + } + } + + private static Dictionary CollectWorkItems( + ImmutableArray fieldCandidates, + ImmutableArray typeCandidates, + ImmutableArray descriptors, + INamedTypeSymbol? getAllAttribute) + { + var workItems = new Dictionary(SymbolEqualityComparer.Default); + + foreach (var candidate in fieldCandidates + .Where(static candidate => candidate is not null) + .Select(static candidate => candidate!)) + { + if (ResolveExplicitBindings(candidate.FieldSymbol, descriptors).Length == 0) + continue; + + var typeSymbol = candidate.FieldSymbol.ContainingType; + if (!workItems.TryGetValue(typeSymbol, out var workItem)) + { + workItem = new TypeWorkItem(typeSymbol); + workItems.Add(typeSymbol, workItem); + } + + workItem.FieldCandidates.Add(candidate); + } + + if (getAllAttribute is null) + return workItems; + + foreach (var candidate in typeCandidates + .Where(static candidate => candidate is not null) + .Select(static candidate => candidate!)) + { + if (!candidate.TypeSymbol.GetAttributes().Any(attribute => + SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, getAllAttribute))) + continue; + + if (!workItems.TryGetValue(candidate.TypeSymbol, out var workItem)) + { + workItem = new TypeWorkItem(candidate.TypeSymbol); + workItems.Add(candidate.TypeSymbol, workItem); + } + + workItem.GetAllDeclaration ??= candidate.Declaration; + } + + return workItems; + } + + private static bool CanGenerateForType( + SourceProductionContext context, + TypeWorkItem workItem, + ContextSymbols symbols) + { + if (workItem.TypeSymbol.ContainingType is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + ContextGetDiagnostics.NestedClassNotSupported, + GetTypeLocation(workItem), + workItem.TypeSymbol.Name)); + return false; + } + + if (!workItem.TypeSymbol.AreAllDeclarationsPartial()) + { + context.ReportDiagnostic(Diagnostic.Create( + CommonDiagnostics.ClassMustBePartial, + GetTypeLocation(workItem), + workItem.TypeSymbol.Name)); + return false; + } + + if (IsContextAwareType(workItem.TypeSymbol, symbols)) + return true; + + context.ReportDiagnostic(Diagnostic.Create( + ContextGetDiagnostics.ContextAwareTypeRequired, + GetTypeLocation(workItem), + workItem.TypeSymbol.Name)); + return false; + } + + private static bool IsContextAwareType( + INamedTypeSymbol typeSymbol, + ContextSymbols symbols) + { + if (symbols.ContextAwareAttribute is not null && + typeSymbol.GetAttributes().Any(attribute => + SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbols.ContextAwareAttribute))) + return true; + + return typeSymbol.IsAssignableTo(symbols.IContextAware) || + typeSymbol.IsAssignableTo(symbols.ContextAwareBase); + } + + private static ImmutableArray ResolveBindingDescriptors(Compilation compilation) + { + var builder = ImmutableArray.CreateBuilder(BindingDescriptors.Length); + + foreach (var descriptor in BindingDescriptors) + { + var attributeSymbol = compilation.GetTypeByMetadataName(descriptor.MetadataName); + if (attributeSymbol is null) + continue; + + builder.Add(new ResolvedBindingDescriptor(descriptor, attributeSymbol)); + } + + return builder.ToImmutable(); + } + + private static ImmutableArray ResolveExplicitBindings( + IFieldSymbol fieldSymbol, + ImmutableArray descriptors) + { + if (descriptors.IsDefaultOrEmpty) + return []; + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var descriptor in descriptors.Where(descriptor => fieldSymbol.GetAttributes().Any(attribute => + SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, descriptor.AttributeSymbol)))) + { + builder.Add(descriptor); + } + + return builder.ToImmutable(); + } + + private static bool TryCreateExplicitBinding( + SourceProductionContext context, + FieldCandidateInfo candidate, + ResolvedBindingDescriptor descriptor, + ContextSymbols symbols, + out BindingInfo binding) + { + binding = default; + + if (candidate.FieldSymbol.IsStatic) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.StaticFieldNotSupported, + candidate); + return false; + } + + if (candidate.FieldSymbol.IsReadOnly) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.ReadOnlyFieldNotSupported, + candidate); + return false; + } + + if (!TryResolveBindingTarget(candidate.FieldSymbol.Type, descriptor.Definition.Kind, symbols, + out var targetType)) + { + context.ReportDiagnostic(Diagnostic.Create( + ContextGetDiagnostics.InvalidBindingType, + candidate.Variable.Identifier.GetLocation(), + candidate.FieldSymbol.Name, + candidate.FieldSymbol.Type.ToDisplayString(), + descriptor.Definition.AttributeName)); + return false; + } + + binding = new BindingInfo(candidate.FieldSymbol, descriptor.Definition.Kind, targetType); + return true; + } + + private static bool TryCreateInferredBinding( + IFieldSymbol fieldSymbol, + ContextSymbols symbols, + out BindingInfo binding) + { + binding = default; + + if (symbols.GodotNode is not null && fieldSymbol.Type.IsAssignableTo(symbols.GodotNode)) + return false; + + if (TryResolveCollectionElement(fieldSymbol.Type, symbols.IReadOnlyList, out var elementType)) + { + if (elementType.IsAssignableTo(symbols.IModel)) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Models, elementType); + return true; + } + + if (elementType.IsAssignableTo(symbols.ISystem)) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Systems, elementType); + return true; + } + + if (elementType.IsAssignableTo(symbols.IUtility)) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Utilities, elementType); + return true; + } + + if (elementType.IsReferenceType) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Services, elementType); + return true; + } + + return false; + } + + if (fieldSymbol.Type.IsAssignableTo(symbols.IModel)) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Model, fieldSymbol.Type); + return true; + } + + if (fieldSymbol.Type.IsAssignableTo(symbols.ISystem)) + { + binding = new BindingInfo(fieldSymbol, BindingKind.System, fieldSymbol.Type); + return true; + } + + if (fieldSymbol.Type.IsAssignableTo(symbols.IUtility)) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Utility, fieldSymbol.Type); + return true; + } + + if (fieldSymbol.Type.IsReferenceType) + { + binding = new BindingInfo(fieldSymbol, BindingKind.Service, fieldSymbol.Type); + return true; + } + + return false; + } + + private static bool TryResolveBindingTarget( + ITypeSymbol fieldType, + BindingKind kind, + ContextSymbols symbols, + out ITypeSymbol targetType) + { + targetType = null!; + + switch (kind) + { + case BindingKind.Service: + if (!fieldType.IsReferenceType) + return false; + + targetType = fieldType; + return true; + + case BindingKind.Model: + if (!fieldType.IsAssignableTo(symbols.IModel)) + return false; + + targetType = fieldType; + return true; + + case BindingKind.System: + if (!fieldType.IsAssignableTo(symbols.ISystem)) + return false; + + targetType = fieldType; + return true; + + case BindingKind.Utility: + if (!fieldType.IsAssignableTo(symbols.IUtility)) + return false; + + targetType = fieldType; + return true; + + case BindingKind.Services: + return TryResolveReferenceCollection(fieldType, symbols.IReadOnlyList, out targetType); + + case BindingKind.Models: + return TryResolveConstrainedCollection(fieldType, symbols.IReadOnlyList, symbols.IModel, + out targetType); + + case BindingKind.Systems: + return TryResolveConstrainedCollection(fieldType, symbols.IReadOnlyList, symbols.ISystem, + out targetType); + + case BindingKind.Utilities: + return TryResolveConstrainedCollection(fieldType, symbols.IReadOnlyList, symbols.IUtility, + out targetType); + + default: + return false; + } + } + + private static bool TryResolveReferenceCollection( + ITypeSymbol fieldType, + INamedTypeSymbol? readOnlyList, + out ITypeSymbol elementType) + { + elementType = null!; + + if (!TryResolveCollectionElement(fieldType, readOnlyList, out var candidate)) + return false; + + if (!candidate.IsReferenceType) + return false; + + elementType = candidate; + return true; + } + + private static bool TryResolveConstrainedCollection( + ITypeSymbol fieldType, + INamedTypeSymbol? readOnlyList, + INamedTypeSymbol? constraintType, + out ITypeSymbol elementType) + { + elementType = null!; + + if (!TryResolveCollectionElement(fieldType, readOnlyList, out var candidate)) + return false; + + if (!candidate.IsAssignableTo(constraintType)) + return false; + + elementType = candidate; + return true; + } + + private static bool TryResolveCollectionElement( + ITypeSymbol fieldType, + INamedTypeSymbol? readOnlyList, + out ITypeSymbol elementType) + { + elementType = null!; + + if (readOnlyList is null || fieldType is not INamedTypeSymbol namedType) + return false; + + if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, readOnlyList)) + return false; + + if (namedType.TypeArguments.Length != 1) + return false; + + elementType = namedType.TypeArguments[0]; + return true; + } + + private static IEnumerable GetAllFields(INamedTypeSymbol typeSymbol) + { + return typeSymbol.GetMembers() + .OfType() + .Where(static field => !field.IsImplicitlyDeclared) + .OrderBy(static field => field.Locations.FirstOrDefault()?.SourceSpan.Start ?? int.MaxValue) + .ThenBy(static field => field.Name, StringComparer.Ordinal); + } + + private static void ReportFieldDiagnostic( + SourceProductionContext context, + DiagnosticDescriptor descriptor, + FieldCandidateInfo candidate) + { + context.ReportDiagnostic(Diagnostic.Create( + descriptor, + candidate.Variable.Identifier.GetLocation(), + candidate.FieldSymbol.Name)); + } + + private static void ReportFieldDiagnostic( + SourceProductionContext context, + DiagnosticDescriptor descriptor, + IFieldSymbol fieldSymbol) + { + context.ReportDiagnostic(Diagnostic.Create( + descriptor, + fieldSymbol.Locations.FirstOrDefault() ?? Location.None, + fieldSymbol.Name)); + } + + private static Location GetTypeLocation(TypeWorkItem workItem) + { + if (workItem.GetAllDeclaration is not null) + return workItem.GetAllDeclaration.Identifier.GetLocation(); + + return workItem.FieldCandidates[0].Variable.Identifier.GetLocation(); + } + + private static string GenerateSource( + INamedTypeSymbol typeSymbol, + IReadOnlyList bindings) + { + var namespaceName = typeSymbol.GetNamespace(); + var generics = typeSymbol.ResolveGenerics(); + var orderedBindings = bindings + .OrderBy(static binding => binding.Field.Locations.FirstOrDefault()?.SourceSpan.Start ?? int.MaxValue) + .ThenBy(static binding => binding.Field.Name, StringComparer.Ordinal) + .ToList(); + + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + sb.AppendLine("using GFramework.Core.Extensions;"); + sb.AppendLine(); + + if (namespaceName is not null) + { + sb.AppendLine($"namespace {namespaceName};"); + sb.AppendLine(); + } + + sb.AppendLine($"partial class {typeSymbol.Name}{generics.Parameters}"); + foreach (var constraint in generics.Constraints) + sb.AppendLine($" {constraint}"); + + sb.AppendLine("{"); + sb.AppendLine($" private void {InjectionMethodName}()"); + sb.AppendLine(" {"); + + foreach (var binding in orderedBindings) + { + var targetType = binding.TargetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sb.AppendLine($" {binding.Field.Name} = {ResolveAccessor(binding.Kind, targetType)};"); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + return sb.ToString(); + } + + private static string ResolveAccessor(BindingKind kind, string targetType) + { + return kind switch + { + BindingKind.Service => $"this.GetService<{targetType}>()", + BindingKind.Services => $"this.GetServices<{targetType}>()", + BindingKind.System => $"this.GetSystem<{targetType}>()", + BindingKind.Systems => $"this.GetSystems<{targetType}>()", + BindingKind.Model => $"this.GetModel<{targetType}>()", + BindingKind.Models => $"this.GetModels<{targetType}>()", + BindingKind.Utility => $"this.GetUtility<{targetType}>()", + BindingKind.Utilities => $"this.GetUtilities<{targetType}>()", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) + }; + } + + private static string GetHintName(INamedTypeSymbol typeSymbol) + { + var hintName = typeSymbol.GetNamespace() is { Length: > 0 } namespaceName + ? $"{namespaceName}.{typeSymbol.GetFullClassName()}" + : typeSymbol.GetFullClassName(); + + return hintName.Replace('.', '_') + ".ContextGet.g.cs"; + } + + private enum BindingKind + { + Service, + Services, + System, + Systems, + Model, + Models, + Utility, + Utilities + } + + private sealed record BindingDescriptor( + BindingKind Kind, + string MetadataName, + string AttributeName, + bool IsCollection); + + private readonly record struct ResolvedBindingDescriptor( + BindingDescriptor Definition, + INamedTypeSymbol AttributeSymbol); + + private readonly record struct BindingInfo( + IFieldSymbol Field, + BindingKind Kind, + ITypeSymbol TargetType); + + private readonly record struct ContextSymbols( + INamedTypeSymbol? ContextAwareAttribute, + INamedTypeSymbol? IContextAware, + INamedTypeSymbol? ContextAwareBase, + INamedTypeSymbol? IModel, + INamedTypeSymbol? ISystem, + INamedTypeSymbol? IUtility, + INamedTypeSymbol? IReadOnlyList, + INamedTypeSymbol? GodotNode); + + private sealed class TypeWorkItem(INamedTypeSymbol typeSymbol) + { + public INamedTypeSymbol TypeSymbol { get; } = typeSymbol; + public List FieldCandidates { get; } = []; + public ClassDeclarationSyntax? GetAllDeclaration { get; set; } + } +} \ No newline at end of file From ee2936e0a24c265017d9b67cf5b6aa4095ea585b Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:47:59 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(generator):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E8=8E=B7=E5=8F=96=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ContextSymbols 的创建提取为独立方法 CreateContextSymbols - 将源码生成逻辑提取为独立方法 GenerateSources - 将绑定收集逻辑提取为独立方法 CollectBindings - 将显式绑定添加逻辑提取为独立方法 AddExplicitBindings - 将推断绑定添加逻辑提取为独立方法 AddInferredBindings - 将绑定推断检查逻辑提取为独立方法 CanInferBinding - 优化了代码组织结构和可读性 --- .../Rule/ContextGetGenerator.cs | 194 +++++++++++------- 1 file changed, 121 insertions(+), 73 deletions(-) diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs index d2beb2a..902b0df 100644 --- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -171,11 +171,22 @@ public sealed class ContextGetGenerator : IIncrementalGenerator var descriptors = ResolveBindingDescriptors(compilation); var getAllAttribute = compilation.GetTypeByMetadataName(GetAllAttributeMetadataName); - if (descriptors.Length == 0 && getAllAttribute is null) return; - var symbols = new ContextSymbols( + var symbols = CreateContextSymbols(compilation); + var workItems = CollectWorkItems( + fieldCandidates, + typeCandidates, + descriptors, + getAllAttribute); + + GenerateSources(context, descriptors, symbols, workItems); + } + + private static ContextSymbols CreateContextSymbols(Compilation compilation) + { + return new ContextSymbols( compilation.GetTypeByMetadataName(ContextAwareAttributeMetadataName), compilation.GetTypeByMetadataName(IContextAwareMetadataName), compilation.GetTypeByMetadataName(ContextAwareBaseMetadataName), @@ -184,83 +195,20 @@ public sealed class ContextGetGenerator : IIncrementalGenerator compilation.GetTypeByMetadataName(IUtilityMetadataName), compilation.GetTypeByMetadataName(IReadOnlyListMetadataName), compilation.GetTypeByMetadataName(GodotNodeMetadataName)); + } - var workItems = CollectWorkItems( - fieldCandidates, - typeCandidates, - descriptors, - getAllAttribute); - + private static void GenerateSources( + SourceProductionContext context, + ImmutableArray descriptors, + ContextSymbols symbols, + Dictionary workItems) + { foreach (var workItem in workItems.Values) { if (!CanGenerateForType(context, workItem, symbols)) continue; - var bindings = new List(); - var explicitFields = new HashSet(SymbolEqualityComparer.Default); - - foreach (var candidate in workItem.FieldCandidates - .OrderBy(static candidate => candidate.Variable.SpanStart) - .ThenBy(static candidate => candidate.FieldSymbol.Name, StringComparer.Ordinal)) - { - var matches = ResolveExplicitBindings(candidate.FieldSymbol, descriptors); - if (matches.Length == 0) - continue; - - explicitFields.Add(candidate.FieldSymbol); - - if (matches.Length > 1) - { - ReportFieldDiagnostic( - context, - ContextGetDiagnostics.MultipleBindingAttributesNotSupported, - candidate); - continue; - } - - if (!TryCreateExplicitBinding( - context, - candidate, - matches[0], - symbols, - out var binding)) - continue; - - bindings.Add(binding); - } - - if (workItem.GetAllDeclaration is not null) - { - foreach (var field in GetAllFields(workItem.TypeSymbol)) - { - if (explicitFields.Contains(field)) - continue; - - if (field.IsStatic) - { - ReportFieldDiagnostic( - context, - ContextGetDiagnostics.StaticFieldNotSupported, - field); - continue; - } - - if (field.IsReadOnly) - { - ReportFieldDiagnostic( - context, - ContextGetDiagnostics.ReadOnlyFieldNotSupported, - field); - continue; - } - - if (!TryCreateInferredBinding(field, symbols, out var binding)) - continue; - - bindings.Add(binding); - } - } - + var bindings = CollectBindings(context, workItem, descriptors, symbols); if (bindings.Count == 0 && workItem.GetAllDeclaration is null) continue; @@ -269,6 +217,106 @@ public sealed class ContextGetGenerator : IIncrementalGenerator } } + private static List CollectBindings( + SourceProductionContext context, + TypeWorkItem workItem, + ImmutableArray descriptors, + ContextSymbols symbols) + { + var bindings = new List(); + var explicitFields = new HashSet(SymbolEqualityComparer.Default); + + AddExplicitBindings(context, workItem, descriptors, symbols, bindings, explicitFields); + AddInferredBindings(context, workItem, symbols, bindings, explicitFields); + + return bindings; + } + + private static void AddExplicitBindings( + SourceProductionContext context, + TypeWorkItem workItem, + ImmutableArray descriptors, + ContextSymbols symbols, + ICollection bindings, + ISet explicitFields) + { + foreach (var candidate in workItem.FieldCandidates + .OrderBy(static candidate => candidate.Variable.SpanStart) + .ThenBy(static candidate => candidate.FieldSymbol.Name, StringComparer.Ordinal)) + { + var matches = ResolveExplicitBindings(candidate.FieldSymbol, descriptors); + if (matches.Length == 0) + continue; + + explicitFields.Add(candidate.FieldSymbol); + + if (matches.Length > 1) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.MultipleBindingAttributesNotSupported, + candidate); + continue; + } + + if (!TryCreateExplicitBinding( + context, + candidate, + matches[0], + symbols, + out var binding)) + continue; + + bindings.Add(binding); + } + } + + private static void AddInferredBindings( + SourceProductionContext context, + TypeWorkItem workItem, + ContextSymbols symbols, + ICollection bindings, + ISet explicitFields) + { + if (workItem.GetAllDeclaration is null) + return; + + foreach (var field in GetAllFields(workItem.TypeSymbol)) + { + if (explicitFields.Contains(field)) + continue; + + if (!CanInferBinding(context, field)) + continue; + + if (!TryCreateInferredBinding(field, symbols, out var binding)) + continue; + + bindings.Add(binding); + } + } + + private static bool CanInferBinding(SourceProductionContext context, IFieldSymbol field) + { + if (field.IsStatic) + { + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.StaticFieldNotSupported, + field); + return false; + } + + if (!field.IsReadOnly) + return true; + + ReportFieldDiagnostic( + context, + ContextGetDiagnostics.ReadOnlyFieldNotSupported, + field); + return false; + } + private static Dictionary CollectWorkItems( ImmutableArray fieldCandidates, ImmutableArray typeCandidates, From 796408539e1933acc4a211e5c566a089e1b4ebd4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:54:17 +0800 Subject: [PATCH 3/5] =?UTF-8?q?refactor(generators):=20=E4=BC=98=E5=8C=96C?= =?UTF-8?q?ontextGetGenerator=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E5=B9=B6?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改GetService方法文档,将返回值描述从"返回null"改为"抛出异常" - 为GetService方法添加InvalidOperationException异常说明 - 删除冗余的IsExternalInit类文件 - 重构属性匹配逻辑,使用预定义集合进行候选属性名称验证 - 添加辅助方法HasCandidateAttribute、TryGetAttributeSimpleName等提升代码可读性 - 改进集合类型推断逻辑,支持接口类型的遍历匹配 - 更新单元测试以验证完全限定名属性和只读列表类型的支持 - 修正诊断错误位置信息的准确性 --- .../ContextAwareServiceExtensions.cs | 3 +- .../Rule/ContextGetGeneratorTests.cs | 163 +++++++++++++++++- .../GFramework.SourceGenerators.csproj | 5 + .../Internals/IsExternalInit.cs | 20 --- .../Rule/ContextGetGenerator.cs | 109 ++++++++++-- 5 files changed, 262 insertions(+), 38 deletions(-) delete mode 100644 GFramework.SourceGenerators/Internals/IsExternalInit.cs diff --git a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs index e9a099d..27e16e7 100644 --- a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs @@ -18,8 +18,9 @@ public static class ContextAwareServiceExtensions /// /// 要获取的服务类型 /// 实现 IContextAware 接口的上下文感知对象 - /// 指定类型的服务实例,如果未找到则返回 null + /// 指定类型的服务实例,如果未找到则抛出异常 /// 当 contextAware 参数为 null 时抛出 + /// 当指定服务未注册时抛出 public static TService GetService(this IContextAware contextAware) where TService : class { ArgumentNullException.ThrowIfNull(contextAware); diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs index ee49761..daae4e6 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs @@ -101,6 +101,87 @@ public class ContextGetGeneratorTests Assert.Pass(); } + [Test] + public async Task Generates_Bindings_For_Fully_Qualified_Field_Attributes() + { + var source = """ + using System; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + [ContextAware] + public partial class InventoryPanel + { + [global::GFramework.SourceGenerators.Abstractions.Rule.GetModel] + private IInventoryModel _model = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class InventoryPanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_InventoryPanel.ContextGet.g.cs", expected)); + Assert.Pass(); + } + [Test] public async Task Generates_Inferred_Bindings_For_GetAll_Class() { @@ -345,7 +426,7 @@ public class ContextGetGeneratorTests }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error) - .WithSpan(31, 30, 31, 45) + .WithSpan(40, 33, 40, 39) .WithArguments("InventoryPanel")); await test.RunAsync(); @@ -416,10 +497,88 @@ public class ContextGetGeneratorTests }; test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error) - .WithSpan(40, 40, 40, 47) + .WithSpan(46, 39, 46, 46) .WithArguments("_models", "System.Collections.Generic.List", "GetModels")); await test.RunAsync(); Assert.Pass(); } + + [Test] + public async Task Generates_Bindings_For_GetModels_Field_Assignable_From_IReadOnlyList() + { + var source = """ + using System; + using System.Collections.Generic; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelsAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + 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 GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static IReadOnlyList GetModels(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModels] + private IEnumerable _models = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class InventoryPanel + { + private void __InjectContextBindings_Generated() + { + _models = this.GetModels(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_InventoryPanel.ContextGet.g.cs", expected)); + Assert.Pass(); + } } \ No newline at end of file diff --git a/GFramework.SourceGenerators/GFramework.SourceGenerators.csproj b/GFramework.SourceGenerators/GFramework.SourceGenerators.csproj index 0d682e0..99240bd 100644 --- a/GFramework.SourceGenerators/GFramework.SourceGenerators.csproj +++ b/GFramework.SourceGenerators/GFramework.SourceGenerators.csproj @@ -32,6 +32,11 @@ + + + + diff --git a/GFramework.SourceGenerators/Internals/IsExternalInit.cs b/GFramework.SourceGenerators/Internals/IsExternalInit.cs deleted file mode 100644 index 8a76104..0000000 --- a/GFramework.SourceGenerators/Internals/IsExternalInit.cs +++ /dev/null @@ -1,20 +0,0 @@ -// IsExternalInit.cs -// This type is required to support init-only setters and record types -// when targeting netstandard2.0 or older frameworks. - -#if !NET5_0_OR_GREATER -using System.ComponentModel; - -// ReSharper disable CheckNamespace - -namespace System.Runtime.CompilerServices; - -/// -/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。 -/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。 -/// -[EditorBrowsable(EditorBrowsableState.Never)] -internal static class IsExternalInit -{ -} -#endif \ No newline at end of file diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs index 902b0df..55a9537 100644 --- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -89,6 +89,20 @@ public sealed class ContextGetGenerator : IIncrementalGenerator true) ]; + private static readonly ImmutableHashSet FieldCandidateAttributeNames = BindingDescriptors + .SelectMany(static descriptor => new[] + { + descriptor.AttributeName, + descriptor.AttributeName + "Attribute" + }) + .ToImmutableHashSet(StringComparer.Ordinal); + + private static readonly ImmutableHashSet TypeCandidateAttributeNames = + [ + "GetAll", + "GetAllAttribute" + ]; + public void Initialize(IncrementalGeneratorInitializationContext context) { var fieldCandidates = context.SyntaxProvider.CreateSyntaxProvider( @@ -125,9 +139,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator }) return false; - return fieldDeclaration.AttributeLists - .SelectMany(static list => list.Attributes) - .Any(static attribute => attribute.Name.ToString().Contains("Get", StringComparison.Ordinal)); + return HasCandidateAttribute(fieldDeclaration.AttributeLists, FieldCandidateAttributeNames); } private static FieldCandidateInfo? TransformField(GeneratorSyntaxContext context) @@ -135,7 +147,10 @@ public sealed class ContextGetGenerator : IIncrementalGenerator if (context.Node is not VariableDeclaratorSyntax variable) return null; - return context.SemanticModel.GetDeclaredSymbol(variable) is IFieldSymbol fieldSymbol + if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) + return null; + + return HasAnyBindingAttribute(fieldSymbol, context.SemanticModel.Compilation) ? new FieldCandidateInfo(variable, fieldSymbol) : null; } @@ -145,9 +160,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator if (node is not ClassDeclarationSyntax classDeclaration) return false; - return classDeclaration.AttributeLists - .SelectMany(static list => list.Attributes) - .Any(static attribute => attribute.Name.ToString().Contains("GetAll", StringComparison.Ordinal)); + return HasCandidateAttribute(classDeclaration.AttributeLists, TypeCandidateAttributeNames); } private static TypeCandidateInfo? TransformType(GeneratorSyntaxContext context) @@ -155,7 +168,10 @@ public sealed class ContextGetGenerator : IIncrementalGenerator if (context.Node is not ClassDeclarationSyntax classDeclaration) return null; - return context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol typeSymbol + if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol) + return null; + + return HasAttribute(typeSymbol, context.SemanticModel.Compilation, GetAllAttributeMetadataName) ? new TypeCandidateInfo(classDeclaration, typeSymbol) : null; } @@ -317,6 +333,54 @@ public sealed class ContextGetGenerator : IIncrementalGenerator return false; } + private static bool HasCandidateAttribute( + SyntaxList attributeLists, + ImmutableHashSet candidateNames) + { + return attributeLists + .SelectMany(static list => list.Attributes) + .Any(attribute => TryGetAttributeSimpleName(attribute.Name, out var name) && candidateNames.Contains(name)); + } + + private static bool TryGetAttributeSimpleName(NameSyntax attributeName, out string name) + { + switch (attributeName) + { + case SimpleNameSyntax simpleName: + name = simpleName.Identifier.ValueText; + return true; + + case QualifiedNameSyntax qualifiedName: + name = qualifiedName.Right.Identifier.ValueText; + return true; + + case AliasQualifiedNameSyntax aliasQualifiedName: + name = aliasQualifiedName.Name.Identifier.ValueText; + return true; + + default: + name = string.Empty; + return false; + } + } + + private static bool HasAnyBindingAttribute(IFieldSymbol fieldSymbol, Compilation compilation) + { + return Enumerable.Any(BindingDescriptors, + descriptor => HasAttribute(fieldSymbol, compilation, descriptor.MetadataName)); + } + + private static bool HasAttribute( + ISymbol symbol, + Compilation compilation, + string metadataName) + { + var attributeSymbol = compilation.GetTypeByMetadataName(metadataName); + return attributeSymbol is not null && + symbol.GetAttributes().Any(attribute => + SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeSymbol)); + } + private static Dictionary CollectWorkItems( ImmutableArray fieldCandidates, ImmutableArray typeCandidates, @@ -654,17 +718,32 @@ public sealed class ContextGetGenerator : IIncrementalGenerator { elementType = null!; - if (readOnlyList is null || fieldType is not INamedTypeSymbol namedType) + if (readOnlyList is null || fieldType is not INamedTypeSymbol targetType) return false; - if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, readOnlyList)) - return false; + foreach (var candidateType in EnumerateCollectionTypeCandidates(targetType)) + { + if (candidateType.TypeArguments.Length != 1) + continue; - if (namedType.TypeArguments.Length != 1) - return false; + var candidateElementType = candidateType.TypeArguments[0]; + var expectedSourceType = readOnlyList.Construct(candidateElementType); + if (!expectedSourceType.IsAssignableTo(targetType)) + continue; - elementType = namedType.TypeArguments[0]; - return true; + elementType = candidateElementType; + return true; + } + + return false; + } + + private static IEnumerable EnumerateCollectionTypeCandidates(INamedTypeSymbol typeSymbol) + { + yield return typeSymbol; + + foreach (var interfaceType in typeSymbol.AllInterfaces) + yield return interfaceType; } private static IEnumerable GetAllFields(INamedTypeSymbol typeSymbol) From b5ac8b2c346b95d21d39b06c3b05ee2419379ff6 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:03:56 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(core):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E6=84=9F=E7=9F=A5=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E4=BB=A5=E6=94=B9=E8=BF=9B=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入通用的 GetRequiredComponent 方法来统一服务、系统、模型和工具的获取逻辑 - 添加对架构上下文组件的空值检查和异常处理 - 实现更清晰的错误消息以指示未注册的组件类型 - 为所有组件类型(服务、系统、模型、工具)添加缺失的单元测试 - 测试验证当上下文返回空组件时抛出正确的 InvalidOperationException - 改进代码可维护性并减少重复的获取组件逻辑 --- .../ContextAwareServiceExtensionsTests.cs | 59 +++++++++++++++++-- .../ContextAwareServiceExtensions.cs | 23 ++++++-- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/GFramework.Core.Tests/Rule/ContextAwareServiceExtensionsTests.cs b/GFramework.Core.Tests/Rule/ContextAwareServiceExtensionsTests.cs index 0a95d13..b0f066f 100644 --- a/GFramework.Core.Tests/Rule/ContextAwareServiceExtensionsTests.cs +++ b/GFramework.Core.Tests/Rule/ContextAwareServiceExtensionsTests.cs @@ -5,9 +5,9 @@ using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; using GFramework.Core.Architectures; -using GFramework.Core.Extensions; using GFramework.Core.Ioc; using GFramework.Core.Rule; +using GFramework.Core.Tests.Architectures; namespace GFramework.Core.Tests.Rule; @@ -18,6 +18,11 @@ namespace GFramework.Core.Tests.Rule; [TestFixture] public class ContextAwareServiceExtensionsTests { + private MicrosoftDiContainer _container = null!; + private ArchitectureContext _context = null!; + + private TestContextAware _contextAware = null!; + [SetUp] public void SetUp() { @@ -34,10 +39,6 @@ public class ContextAwareServiceExtensionsTests _container.Clear(); } - private TestContextAware _contextAware = null!; - private ArchitectureContext _context = null!; - private MicrosoftDiContainer _container = null!; - [Test] public void GetService_Should_Return_Registered_Service() { @@ -53,6 +54,18 @@ public class ContextAwareServiceExtensionsTests Assert.That(result, Is.SameAs(service)); } + [Test] + public void GetService_Should_Throw_When_Context_Returns_Null_Service() + { + // Arrange + var contextAware = new TestContextAware(); + ((IContextAware)contextAware).SetContext(new TestArchitectureContextV3()); + + // Act / Assert + Assert.That(() => contextAware.GetService(), + Throws.InvalidOperationException.With.Message.Contains("Service")); + } + [Test] public void GetSystem_Should_Return_Registered_System() { @@ -68,6 +81,18 @@ public class ContextAwareServiceExtensionsTests Assert.That(result, Is.SameAs(system)); } + [Test] + public void GetSystem_Should_Throw_When_Context_Returns_Null_System() + { + // Arrange + var contextAware = new TestContextAware(); + ((IContextAware)contextAware).SetContext(new TestArchitectureContextV3()); + + // Act / Assert + Assert.That(() => contextAware.GetSystem(), + Throws.InvalidOperationException.With.Message.Contains("System")); + } + [Test] public void GetModel_Should_Return_Registered_Model() { @@ -83,6 +108,18 @@ public class ContextAwareServiceExtensionsTests Assert.That(result, Is.SameAs(model)); } + [Test] + public void GetModel_Should_Throw_When_Context_Returns_Null_Model() + { + // Arrange + var contextAware = new TestContextAware(); + ((IContextAware)contextAware).SetContext(new TestArchitectureContextV3()); + + // Act / Assert + Assert.That(() => contextAware.GetModel(), + Throws.InvalidOperationException.With.Message.Contains("Model")); + } + [Test] public void GetUtility_Should_Return_Registered_Utility() { @@ -98,6 +135,18 @@ public class ContextAwareServiceExtensionsTests Assert.That(result, Is.SameAs(utility)); } + [Test] + public void GetUtility_Should_Throw_When_Context_Returns_Null_Utility() + { + // Arrange + var contextAware = new TestContextAware(); + ((IContextAware)contextAware).SetContext(new TestArchitectureContextV3()); + + // Act / Assert + Assert.That(() => contextAware.GetUtility(), + Throws.InvalidOperationException.With.Message.Contains("Utility")); + } + [Test] public void GetServices_Should_Return_All_Registered_Services() { diff --git a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs index 27e16e7..cb82199 100644 --- a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs @@ -1,3 +1,4 @@ +using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Systems; @@ -25,7 +26,8 @@ public static class ContextAwareServiceExtensions { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); - return context.GetService(); + return GetRequiredComponent(context, static architectureContext => architectureContext.GetService(), + "Service"); } /// @@ -39,7 +41,8 @@ public static class ContextAwareServiceExtensions { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); - return context.GetSystem(); + return GetRequiredComponent(context, static architectureContext => architectureContext.GetSystem(), + "System"); } /// @@ -53,7 +56,8 @@ public static class ContextAwareServiceExtensions { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); - return context.GetModel(); + return GetRequiredComponent(context, static architectureContext => architectureContext.GetModel(), + "Model"); } /// @@ -67,7 +71,8 @@ public static class ContextAwareServiceExtensions { ArgumentNullException.ThrowIfNull(contextAware); var context = contextAware.GetContext(); - return context.GetUtility(); + return GetRequiredComponent(context, static architectureContext => architectureContext.GetUtility(), + "Utility"); } #endregion @@ -194,5 +199,15 @@ public static class ContextAwareServiceExtensions return context.GetUtilitiesByPriority(); } + private static TComponent GetRequiredComponent(IArchitectureContext context, + Func resolver, string componentKind) + where TComponent : class + { + ArgumentNullException.ThrowIfNull(context); + + var component = resolver(context); + return component ?? throw new InvalidOperationException($"{componentKind} {typeof(TComponent)} not registered"); + } + #endregion } \ No newline at end of file From 6e3954eb3e3d74ea39fd6de6d5a2c7176792802c Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:06:01 +0800 Subject: [PATCH 5/5] =?UTF-8?q?docs(GFramework.Core):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20ContextAwareServiceExtensions=20=E6=96=87=E6=A1=A3=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 GetSystem 方法添加 InvalidOperationException 异常文档说明 - 为 GetModel 方法添加 InvalidOperationException 异常文档说明 - 为 GetUtility 方法添加 InvalidOperationException 异常文档说明 - 清理文件末尾多余空行 --- GFramework.Core/Extensions/ContextAwareServiceExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs index cb82199..2b0b8ec 100644 --- a/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareServiceExtensions.cs @@ -37,6 +37,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的对象 /// 指定类型的系统实例 /// 当 contextAware 为 null 时抛出 + /// 当指定系统未注册时抛出 public static TSystem GetSystem(this IContextAware contextAware) where TSystem : class, ISystem { ArgumentNullException.ThrowIfNull(contextAware); @@ -52,6 +53,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的对象 /// 指定类型的模型实例 /// 当 contextAware 为 null 时抛出 + /// 当指定模型未注册时抛出 public static TModel GetModel(this IContextAware contextAware) where TModel : class, IModel { ArgumentNullException.ThrowIfNull(contextAware); @@ -67,6 +69,7 @@ public static class ContextAwareServiceExtensions /// 实现 IContextAware 接口的对象 /// 指定类型的工具实例 /// 当 contextAware 为 null 时抛出 + /// 当指定工具未注册时抛出 public static TUtility GetUtility(this IContextAware contextAware) where TUtility : class, IUtility { ArgumentNullException.ThrowIfNull(contextAware);