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