Merge pull request #148 from GeWuYou/fix/context-get-generator-service-binding-

Fix/context get generator service binding
This commit is contained in:
gewuyou 2026-03-28 19:54:40 +08:00 committed by GitHub
commit 428b932f66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 232 additions and 34 deletions

View File

@ -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;
/// <summary>
/// 提供源代码生成器测试的通用功能。
@ -30,8 +27,21 @@ public static class GeneratorTest<TGenerator>
foreach (var (filename, content) in generatedSources)
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, content));
(typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync();
}
/// <summary>
/// 将测试内联快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。
/// </summary>
/// <param name="content">原始快照内容。</param>
/// <returns>使用当前平台换行符的快照内容。</returns>
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);
}
}

View File

@ -16,3 +16,7 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp.Testing;
global using Microsoft.CodeAnalysis.Testing;
global using NUnit.Framework;

View File

@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
namespace GFramework.SourceGenerators.Tests.Core;
namespace GFramework.SourceGenerators.Tests.Core;
/// <summary>
/// 提供源代码生成器测试的通用功能
@ -32,8 +29,21 @@ public static class GeneratorTest<TGenerator>
// 添加期望的生成源文件到测试状态中
foreach (var (filename, content) in generatedSources)
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, content));
(typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync();
}
/// <summary>
/// 将测试快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。
/// </summary>
/// <param name="content">原始快照内容。</param>
/// <returns>使用当前平台换行符的快照内容。</returns>
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);
}
}

View File

@ -16,3 +16,7 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp.Testing;
global using Microsoft.CodeAnalysis.Testing;
global using NUnit.Framework;

View File

@ -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<T> GetModels<T>(this object contextAware) => default!;
public static T GetSystem<T>(this object contextAware) => default!;
public static T GetUtility<T>(this object contextAware) => default!;
public static T GetService<T>(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<IStrategy> _services = null!;
private Godot.Node _node = null!;
}
}
@ -279,6 +275,96 @@ public class ContextGetGeneratorTests
_models = this.GetModels<global::TestApp.IInventoryModel>();
_system = this.GetSystem<global::TestApp.ICombatSystem>();
_utility = this.GetUtility<global::TestApp.IUiUtility>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.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<T>(this object contextAware) => default!;
public static T GetService<T>(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 = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class BattlePanel
{
private void __InjectContextBindings_Generated()
{
_model = this.GetModel<global::TestApp.IInventoryModel>();
_service = this.GetService<global::TestApp.IStrategy>();
}
}
@ -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<T>(this object contextAware) => default!;
public static T GetSystem<T>(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 = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class GameplayHud
{
private void __InjectContextBindings_Generated()
{
_gridModel = this.GetModel<global::TestApp.IGridModel>();
_runLoopSystem = this.GetSystem<global::TestApp.IRunLoopSystem>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_GameplayHud.ContextGet.g.cs", expected));
Assert.Pass();
}
[Test]
public async Task Generates_Bindings_For_IContextAware_Class()
{

View File

@ -16,3 +16,5 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp.Syntax;

View File

@ -43,4 +43,7 @@ public partial class InventoryPanel
}
```
`[GetAll]` 作用于类本身,会自动扫描字段并推断对应的 `GetX` 调用;已显式标记字段的优先级更高。
`[GetAll]` 作用于类本身,会自动扫描字段并推断 `Model``System``Utility` 相关的 `GetX` 调用;已显式标记字段的优先级更高。
`Service``Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]`
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。

View File

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