From 2ae783c12784a1e665643fdaab2a423cf1f62e4d Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 30 Mar 2026 08:40:57 +0800
Subject: [PATCH] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?=
=?UTF-8?q?=20[GetAll]=20=E7=89=B9=E6=80=A7=E7=9A=84=E9=9D=99=E6=80=81?=
=?UTF-8?q?=E5=92=8C=E5=8F=AA=E8=AF=BB=E5=AD=97=E6=AE=B5=E8=B7=B3=E8=BF=87?=
=?UTF-8?q?=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加了新的诊断规则 GF_ContextGet_007 和 GF_ContextGet_08
- 实现了对静态字段和只读字段的跳过逻辑
- 为 [GetAll] 特性添加了跳过字段的警告提示
- 更新了测试用例验证跳过逻辑的正确性
- 修改了代码生成顺序以确保正确的绑定推断
- 在 README 中添加了关于字段跳过的文档说明
---
.../Constants/PathContests.cs | 6 +
.../Rule/ContextGetGeneratorTests.cs | 423 ++++++++++++++++++
.../AnalyzerReleases.Unshipped.md | 2 +
.../Diagnostics/ContextGetDiagnostics.cs | 38 +-
GFramework.SourceGenerators/GlobalUsings.cs | 5 +-
GFramework.SourceGenerators/README.md | 3 +
.../Rule/ContextGetGenerator.cs | 12 +-
7 files changed, 475 insertions(+), 14 deletions(-)
diff --git a/GFramework.SourceGenerators.Common/Constants/PathContests.cs b/GFramework.SourceGenerators.Common/Constants/PathContests.cs
index e5cd758..344b027 100644
--- a/GFramework.SourceGenerators.Common/Constants/PathContests.cs
+++ b/GFramework.SourceGenerators.Common/Constants/PathContests.cs
@@ -25,6 +25,12 @@ public static class PathContests
///
public const string GameNamespace = $"{BaseNamespace}.Game";
+ ///
+ /// GFramework源代码生成器抽象层命名空间
+ ///
+ public const string SourceGeneratorsPath = $"{BaseNamespace}.SourceGenerators";
+
+
///
/// GFramework源代码生成器抽象层命名空间
///
diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
index 0fc78a9..30d165a 100644
--- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
@@ -377,6 +377,289 @@ public class ContextGetGeneratorTests
Assert.Pass();
}
+ [Test]
+ public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic()
+ {
+ var source = """
+ using System;
+ 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.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!;
+ }
+ }
+
+ namespace TestApp
+ {
+ public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
+
+ [GetAll]
+ public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
+ {
+ private const double LogicStep = 0.2;
+ private IInventoryModel _model = null!;
+ }
+ }
+ """;
+
+ const string expected = """
+ //
+ #nullable enable
+
+ using GFramework.Core.Extensions;
+
+ namespace TestApp;
+
+ partial class BattlePanel
+ {
+ private void __InjectContextBindings_Generated()
+ {
+ _model = this.GetModel();
+ }
+ }
+
+ """;
+
+ await GeneratorTest.RunAsync(
+ source,
+ ("TestApp_BattlePanel.ContextGet.g.cs", expected));
+ Assert.Pass();
+ }
+
+ [Test]
+ public async Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class()
+ {
+ var source = """
+ using System;
+ 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.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 T GetSystem(this object contextAware) => default!;
+ }
+ }
+
+ namespace TestApp
+ {
+ public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
+ public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { }
+
+ [GetAll]
+ public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
+ {
+ private readonly IInventoryModel _model = null!;
+ private ICombatSystem _system = null!;
+ }
+ }
+ """;
+
+ const string expected = """
+ //
+ #nullable enable
+
+ using GFramework.Core.Extensions;
+
+ namespace TestApp;
+
+ partial class BattlePanel
+ {
+ private void __InjectContextBindings_Generated()
+ {
+ _system = this.GetSystem();
+ }
+ }
+
+ """;
+
+ var test = new CSharpSourceGeneratorTest
+ {
+ TestState =
+ {
+ Sources = { source },
+ GeneratedSources =
+ {
+ (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
+ }
+ },
+ DisabledDiagnostics = { "GF_Common_Trace_001" }
+ };
+
+ test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning)
+ .WithSpan(52, 42, 52, 48)
+ .WithArguments("_model"));
+
+ await test.RunAsync();
+ Assert.Pass();
+ }
+
+ [Test]
+ public async Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class()
+ {
+ var source = """
+ using System;
+ 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.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 T GetSystem(this object contextAware) => default!;
+ }
+ }
+
+ namespace TestApp
+ {
+ public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
+ public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { }
+
+ [GetAll]
+ public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
+ {
+ private static IInventoryModel _model = null!;
+ private ICombatSystem _system = null!;
+ }
+ }
+ """;
+
+ const string expected = """
+ //
+ #nullable enable
+
+ using GFramework.Core.Extensions;
+
+ namespace TestApp;
+
+ partial class BattlePanel
+ {
+ private void __InjectContextBindings_Generated()
+ {
+ _system = this.GetSystem();
+ }
+ }
+
+ """;
+
+ var test = new CSharpSourceGeneratorTest
+ {
+ TestState =
+ {
+ Sources = { source },
+ GeneratedSources =
+ {
+ (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
+ }
+ },
+ DisabledDiagnostics = { "GF_Common_Trace_001" }
+ };
+
+ test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning)
+ .WithSpan(52, 40, 52, 46)
+ .WithArguments("_model"));
+
+ await test.RunAsync();
+ Assert.Pass();
+ }
+
[Test]
public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
{
@@ -682,6 +965,146 @@ public class ContextGetGeneratorTests
Assert.Pass();
}
+ [Test]
+ public async Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field()
+ {
+ 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.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 { }
+
+ public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
+ {
+ [GetModel]
+ private readonly IInventoryModel _model = null!;
+ }
+ }
+ """;
+
+ var test = new CSharpSourceGeneratorTest
+ {
+ TestState =
+ {
+ Sources = { source }
+ },
+ DisabledDiagnostics = { "GF_Common_Trace_001" }
+ };
+
+ test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error)
+ .WithSpan(45, 42, 45, 48)
+ .WithArguments("_model"));
+
+ await test.RunAsync();
+ Assert.Pass();
+ }
+
+ [Test]
+ public async Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field()
+ {
+ 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.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 { }
+
+ public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
+ {
+ [GetModel]
+ private static IInventoryModel _model = null!;
+ }
+ }
+ """;
+
+ var test = new CSharpSourceGeneratorTest
+ {
+ TestState =
+ {
+ Sources = { source }
+ },
+ DisabledDiagnostics = { "GF_Common_Trace_001" }
+ };
+
+ test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error)
+ .WithSpan(45, 40, 45, 46)
+ .WithArguments("_model"));
+
+ await test.RunAsync();
+ Assert.Pass();
+ }
+
[Test]
public async Task Generates_Bindings_For_GetModels_Field_Assignable_From_IReadOnlyList()
{
diff --git a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
index 24f780f..8b25a37 100644
--- a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -13,6 +13,8 @@
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_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
+ GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
diff --git a/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs b/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs
index 6735ac5..1f18ac6 100644
--- a/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs
+++ b/GFramework.SourceGenerators/Diagnostics/ContextGetDiagnostics.cs
@@ -1,4 +1,4 @@
-using Microsoft.CodeAnalysis;
+using GFramework.SourceGenerators.Common.Constants;
namespace GFramework.SourceGenerators.Diagnostics;
@@ -7,6 +7,8 @@ namespace GFramework.SourceGenerators.Diagnostics;
///
public static class ContextGetDiagnostics
{
+ private const string SourceGeneratorsRuleCategory = $"{PathContests.SourceGeneratorsPath}.Rule";
+
///
/// 不支持在嵌套类中生成注入代码。
///
@@ -14,7 +16,7 @@ public static class ContextGetDiagnostics
"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",
+ SourceGeneratorsRuleCategory,
DiagnosticSeverity.Error,
true);
@@ -25,7 +27,7 @@ public static class ContextGetDiagnostics
"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",
+ SourceGeneratorsRuleCategory,
DiagnosticSeverity.Error,
true);
@@ -36,10 +38,32 @@ public static class ContextGetDiagnostics
"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",
+ SourceGeneratorsRuleCategory,
DiagnosticSeverity.Error,
true);
+ ///
+ /// 使用 [GetAll] 时,静态字段会被跳过且不会生成注入赋值。
+ ///
+ public static readonly DiagnosticDescriptor GetAllStaticFieldSkipped = new(
+ "GF_ContextGet_007",
+ "Static field will be skipped by [GetAll] context Get injection",
+ "Field '{0}' is static and will be skipped by [GetAll] context Get injection generation",
+ SourceGeneratorsRuleCategory,
+ DiagnosticSeverity.Warning,
+ true);
+
+ ///
+ /// 使用 [GetAll] 时,只读字段会被跳过且不会生成注入赋值。
+ ///
+ public static readonly DiagnosticDescriptor GetAllReadOnlyFieldSkipped = new(
+ "GF_ContextGet_008",
+ "Readonly field will be skipped by [GetAll] context Get injection",
+ "Field '{0}' is readonly and will be skipped by [GetAll] context Get injection generation",
+ SourceGeneratorsRuleCategory,
+ DiagnosticSeverity.Warning,
+ true);
+
///
/// 字段类型与注入特性不匹配。
///
@@ -47,7 +71,7 @@ public static class ContextGetDiagnostics
"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",
+ SourceGeneratorsRuleCategory,
DiagnosticSeverity.Error,
true);
@@ -58,7 +82,7 @@ public static class ContextGetDiagnostics
"GF_ContextGet_005",
"Context-aware type is required",
"Class '{0}' must be context-aware to use generated context Get injection",
- "GFramework.SourceGenerators.Rule",
+ SourceGeneratorsRuleCategory,
DiagnosticSeverity.Error,
true);
@@ -69,7 +93,7 @@ public static class ContextGetDiagnostics
"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",
+ SourceGeneratorsRuleCategory,
DiagnosticSeverity.Error,
true);
}
\ No newline at end of file
diff --git a/GFramework.SourceGenerators/GlobalUsings.cs b/GFramework.SourceGenerators/GlobalUsings.cs
index 32c7b84..a23ec42 100644
--- a/GFramework.SourceGenerators/GlobalUsings.cs
+++ b/GFramework.SourceGenerators/GlobalUsings.cs
@@ -16,5 +16,8 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
+global using System.Collections.Immutable;
global using Microsoft.CodeAnalysis;
-global using Microsoft.CodeAnalysis.CSharp.Syntax;
\ No newline at end of file
+global using Microsoft.CodeAnalysis.CSharp.Syntax;
+global using Microsoft.CodeAnalysis.CSharp;
+global using Microsoft.CodeAnalysis.Text;
\ No newline at end of file
diff --git a/GFramework.SourceGenerators/README.md b/GFramework.SourceGenerators/README.md
index 94ace29..6578ddb 100644
--- a/GFramework.SourceGenerators/README.md
+++ b/GFramework.SourceGenerators/README.md
@@ -47,3 +47,6 @@ public partial class InventoryPanel
`Service` 和 `Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]` 或
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。
+
+`[GetAll]` 会跳过 `const`、`static` 和 `readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为
+`Model`、`System` 或 `Utility` 绑定,但因为字段不可赋值而被跳过,生成器会发出警告提示该字段不会参与生成。
diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs
index 4180f38..6d47253 100644
--- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs
+++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs
@@ -1,4 +1,3 @@
-using System.Collections.Immutable;
using System.Text;
using GFramework.SourceGenerators.Common.Constants;
using GFramework.SourceGenerators.Common.Diagnostics;
@@ -299,23 +298,24 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
if (explicitFields.Contains(field))
continue;
- if (!CanInferBinding(context, field))
+ // Infer the target first so [GetAll] only warns for fields it would otherwise bind.
+ if (!TryCreateInferredBinding(field, symbols, out var binding))
continue;
- if (!TryCreateInferredBinding(field, symbols, out var binding))
+ if (!CanApplyInferredBinding(context, field))
continue;
bindings.Add(binding);
}
}
- private static bool CanInferBinding(SourceProductionContext context, IFieldSymbol field)
+ private static bool CanApplyInferredBinding(SourceProductionContext context, IFieldSymbol field)
{
if (field.IsStatic)
{
ReportFieldDiagnostic(
context,
- ContextGetDiagnostics.StaticFieldNotSupported,
+ ContextGetDiagnostics.GetAllStaticFieldSkipped,
field);
return false;
}
@@ -325,7 +325,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
ReportFieldDiagnostic(
context,
- ContextGetDiagnostics.ReadOnlyFieldNotSupported,
+ ContextGetDiagnostics.GetAllReadOnlyFieldSkipped,
field);
return false;
}