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] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=E4=B8=8A?=
=?UTF-8?q?=E4=B8=8B=E6=96=87=E6=84=9F=E7=9F=A5=E6=B3=A8=E5=85=A5=E6=BA=90?=
=?UTF-8?q?=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