diff --git a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs index 8e98e31..cca8546 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs @@ -1,7 +1,4 @@ -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; - -namespace GFramework.Godot.SourceGenerators.Tests.Core; +namespace GFramework.Godot.SourceGenerators.Tests.Core; /// /// 提供源代码生成器测试的通用功能。 @@ -30,8 +27,21 @@ public static class GeneratorTest foreach (var (filename, content) in generatedSources) test.TestState.GeneratedSources.Add( - (typeof(TGenerator), filename, content)); + (typeof(TGenerator), filename, NormalizeLineEndings(content))); await test.RunAsync(); } + + /// + /// 将测试内联快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。 + /// + /// 原始快照内容。 + /// 使用当前平台换行符的快照内容。 + private static string NormalizeLineEndings(string content) + { + return content + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace("\r", "\n", StringComparison.Ordinal) + .Replace("\n", Environment.NewLine, StringComparison.Ordinal); + } } \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators.Tests/GlobalUsings.cs b/GFramework.Godot.SourceGenerators.Tests/GlobalUsings.cs index 4d27181..e8da86b 100644 --- a/GFramework.Godot.SourceGenerators.Tests/GlobalUsings.cs +++ b/GFramework.Godot.SourceGenerators.Tests/GlobalUsings.cs @@ -15,4 +15,8 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp.Testing; +global using Microsoft.CodeAnalysis.Testing; +global using NUnit.Framework; \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs index 29138e1..a622d38 100644 --- a/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs @@ -1,7 +1,4 @@ -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; - -namespace GFramework.SourceGenerators.Tests.Core; +namespace GFramework.SourceGenerators.Tests.Core; /// /// 提供源代码生成器测试的通用功能 @@ -32,8 +29,21 @@ public static class GeneratorTest // 添加期望的生成源文件到测试状态中 foreach (var (filename, content) in generatedSources) test.TestState.GeneratedSources.Add( - (typeof(TGenerator), filename, content)); + (typeof(TGenerator), filename, NormalizeLineEndings(content))); await test.RunAsync(); } + + /// + /// 将测试快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。 + /// + /// 原始快照内容。 + /// 使用当前平台换行符的快照内容。 + private static string NormalizeLineEndings(string content) + { + return content + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace("\r", "\n", StringComparison.Ordinal) + .Replace("\n", Environment.NewLine, StringComparison.Ordinal); + } } \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/GlobalUsings.cs b/GFramework.SourceGenerators.Tests/GlobalUsings.cs index 4d27181..e8da86b 100644 --- a/GFramework.SourceGenerators.Tests/GlobalUsings.cs +++ b/GFramework.SourceGenerators.Tests/GlobalUsings.cs @@ -15,4 +15,8 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp.Testing; +global using Microsoft.CodeAnalysis.Testing; +global using NUnit.Framework; \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs index daae4e6..0fc78a9 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs @@ -1,9 +1,5 @@ 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; @@ -234,7 +230,6 @@ public class ContextGetGeneratorTests 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!; } } @@ -258,6 +253,7 @@ public class ContextGetGeneratorTests private ICombatSystem _system = null!; private IUiUtility _utility = null!; private IStrategy _service = null!; + private IReadOnlyList _services = null!; private Godot.Node _node = null!; } } @@ -279,6 +275,96 @@ public class ContextGetGeneratorTests _models = this.GetModels(); _system = this.GetSystem(); _utility = this.GetUtility(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_BattlePanel.ContextGet.g.cs", expected)); + Assert.Pass(); + } + + [Test] + public async Task Generates_Explicit_Service_Binding_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 { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetServiceAttribute : 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 GetService(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + public interface IStrategy { } + + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private IInventoryModel _model = null!; + + [GetService] + private IStrategy _service = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class BattlePanel + { + private void __InjectContextBindings_Generated() + { + _model = this.GetModel(); _service = this.GetService(); } } @@ -291,6 +377,98 @@ public class ContextGetGeneratorTests Assert.Pass(); } + [Test] + public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class() + { + var source = """ + using System; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GetAllAttribute : 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 T GetSystem(this object contextAware) => default!; + } + } + + namespace Godot + { + public class Control { } + } + + namespace TestApp + { + public interface IGridModel : GFramework.Core.Abstractions.Model.IModel { } + public interface IRunLoopSystem : GFramework.Core.Abstractions.Systems.ISystem { } + public interface IUiPageBehavior { } + + [ContextAware] + [GetAll] + public partial class GameplayHud : Godot.Control + { + private IGridModel _gridModel = null!; + private IUiPageBehavior? _page; + private IRunLoopSystem _runLoopSystem = null!; + } + } + """; + + const string expected = """ + // + #nullable enable + + using GFramework.Core.Extensions; + + namespace TestApp; + + partial class GameplayHud + { + private void __InjectContextBindings_Generated() + { + _gridModel = this.GetModel(); + _runLoopSystem = this.GetSystem(); + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_GameplayHud.ContextGet.g.cs", expected)); + Assert.Pass(); + } + [Test] public async Task Generates_Bindings_For_IContextAware_Class() { diff --git a/GFramework.SourceGenerators/GlobalUsings.cs b/GFramework.SourceGenerators/GlobalUsings.cs index 4d27181..32c7b84 100644 --- a/GFramework.SourceGenerators/GlobalUsings.cs +++ b/GFramework.SourceGenerators/GlobalUsings.cs @@ -15,4 +15,6 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp.Syntax; \ No newline at end of file diff --git a/GFramework.SourceGenerators/README.md b/GFramework.SourceGenerators/README.md index 59e3d47..94ace29 100644 --- a/GFramework.SourceGenerators/README.md +++ b/GFramework.SourceGenerators/README.md @@ -43,4 +43,7 @@ public partial class InventoryPanel } ``` -`[GetAll]` 作用于类本身,会自动扫描字段并推断对应的 `GetX` 调用;已显式标记字段的优先级更高。 +`[GetAll]` 作用于类本身,会自动扫描字段并推断 `Model`、`System`、`Utility` 相关的 `GetX` 调用;已显式标记字段的优先级更高。 + +`Service` 和 `Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]` 或 +`[GetServices]`,避免将非上下文服务字段误判为服务依赖。 diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs index 55a9537..4180f38 100644 --- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -5,9 +5,6 @@ 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; @@ -582,12 +579,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator return true; } - if (elementType.IsReferenceType) - { - binding = new BindingInfo(fieldSymbol, BindingKind.Services, elementType); - return true; - } - + // Service collections stay opt-in for the same reason as single services. return false; } @@ -609,12 +601,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator return true; } - if (fieldSymbol.Type.IsReferenceType) - { - binding = new BindingInfo(fieldSymbol, BindingKind.Service, fieldSymbol.Type); - return true; - } - + // Service bindings stay opt-in because arbitrary reference types are too ambiguous to infer safely. return false; }