feat(generator): 添加对 [GetAll] 特性的静态和只读字段跳过支持

- 添加了新的诊断规则 GF_ContextGet_007 和 GF_ContextGet_08
- 实现了对静态字段和只读字段的跳过逻辑
- 为 [GetAll] 特性添加了跳过字段的警告提示
- 更新了测试用例验证跳过逻辑的正确性
- 修改了代码生成顺序以确保正确的绑定推断
- 在 README 中添加了关于字段跳过的文档说明
This commit is contained in:
GeWuYou 2026-03-30 08:40:57 +08:00
parent 63cb6b5e17
commit 2ae783c127
7 changed files with 475 additions and 14 deletions

View File

@ -25,6 +25,12 @@ public static class PathContests
/// </summary>
public const string GameNamespace = $"{BaseNamespace}.Game";
/// <summary>
/// GFramework源代码生成器抽象层命名空间
/// </summary>
public const string SourceGeneratorsPath = $"{BaseNamespace}.SourceGenerators";
/// <summary>
/// GFramework源代码生成器抽象层命名空间
/// </summary>

View File

@ -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<T>(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 = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class BattlePanel
{
private void __InjectContextBindings_Generated()
{
_model = this.GetModel<global::TestApp.IInventoryModel>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.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<T>(this object contextAware) => default!;
public static T GetSystem<T>(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 = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class BattlePanel
{
private void __InjectContextBindings_Generated()
{
_system = this.GetSystem<global::TestApp.ICombatSystem>();
}
}
""";
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
{
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<T>(this object contextAware) => default!;
public static T GetSystem<T>(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 = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class BattlePanel
{
private void __InjectContextBindings_Generated()
{
_system = this.GetSystem<global::TestApp.ICombatSystem>();
}
}
""";
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
{
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<T>(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<ContextGetGenerator, DefaultVerifier>
{
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<T>(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<ContextGetGenerator, DefaultVerifier>
{
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()
{

View File

@ -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

View File

@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis;
using GFramework.SourceGenerators.Common.Constants;
namespace GFramework.SourceGenerators.Diagnostics;
@ -7,6 +7,8 @@ namespace GFramework.SourceGenerators.Diagnostics;
/// </summary>
public static class ContextGetDiagnostics
{
private const string SourceGeneratorsRuleCategory = $"{PathContests.SourceGeneratorsPath}.Rule";
/// <summary>
/// 不支持在嵌套类中生成注入代码。
/// </summary>
@ -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);
/// <summary>
/// 使用 <c>[GetAll]</c> 时,静态字段会被跳过且不会生成注入赋值。
/// </summary>
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);
/// <summary>
/// 使用 <c>[GetAll]</c> 时,只读字段会被跳过且不会生成注入赋值。
/// </summary>
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);
/// <summary>
/// 字段类型与注入特性不匹配。
/// </summary>
@ -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);
}

View File

@ -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;
global using Microsoft.CodeAnalysis.CSharp.Syntax;
global using Microsoft.CodeAnalysis.CSharp;
global using Microsoft.CodeAnalysis.Text;

View File

@ -47,3 +47,6 @@ public partial class InventoryPanel
`Service``Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]`
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。
`[GetAll]` 会跳过 `const``static``readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为
`Model``System``Utility` 绑定,但因为字段不可赋值而被跳过,生成器会发出警告提示该字段不会参与生成。

View File

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