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; }