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)