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] =?UTF-8?q?refactor(generators):=20=E4=BC=98=E5=8C=96Conte?= =?UTF-8?q?xtGetGenerator=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)