GFramework/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
GeWuYou 78af119f38 feat(generator): 添加上下文感知注入源码生成器
- 移除ContextAwareServiceExtensions中GetService/GetSystem/GetModel/GetUtility方法的可空返回值
- 添加ContextGetGenerator源码生成器,支持通过特性自动生成上下文注入代码
- 新增GetService/GetServices/GetSystem/GetSystems/GetModel/GetModels/GetUtility/GetUtilities/GetAll特性
- 添加ContextGetDiagnostics提供注入相关的编译时诊断检查
- 实现INamedTypeSymbol扩展方法AreAllDeclarationsPartial用于检查partial类声明
- 添加ITypeSymbol扩展方法IsAssignableTo用于类型兼容性判断
- 创建FieldCandidateInfo和TypeCandidateInfo记录类型用于存储生成器候选信息
- 添加IsExternalInit内部类型支持低版本.NET框架的init-only setter功能
- 更新AnalyzerReleases.Unshipped.md添加新的诊断规则条目
- 创建完整的单元测试验证生成器功能和各种边界情况
2026-03-28 11:29:40 +08:00

425 lines
16 KiB
C#

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;
[TestFixture]
public class ContextGetGeneratorTests
{
[Test]
public async Task Generates_Bindings_For_ContextAwareAttribute_Class()
{
var source = """
using System;
using System.Collections.Generic;
using GFramework.SourceGenerators.Abstractions.Rule;
namespace GFramework.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ContextAwareAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
public sealed class GetModelAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
public sealed class GetServicesAttribute : 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 IReadOnlyList<T> GetServices<T>(this object contextAware) => default!;
}
}
namespace TestApp
{
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
public interface IInventoryStrategy { }
[ContextAware]
public partial class InventoryPanel
{
[GetModel]
private IInventoryModel _model = null!;
[GetServices]
private IReadOnlyList<IInventoryStrategy> _strategies = null!;
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class InventoryPanel
{
private void __InjectContextBindings_Generated()
{
_model = this.GetModel<global::TestApp.IInventoryModel>();
_strategies = this.GetServices<global::TestApp.IInventoryStrategy>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_InventoryPanel.ContextGet.g.cs", expected));
Assert.Pass();
}
[Test]
public async Task Generates_Inferred_Bindings_For_GetAll_Class()
{
var source = """
using System;
using System.Collections.Generic;
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.Abstractions.Architectures
{
public interface IArchitectureContext { }
}
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 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!;
}
}
namespace Godot
{
public class Node { }
}
namespace TestApp
{
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { }
public interface IUiUtility : GFramework.Core.Abstractions.Utility.IUtility { }
public interface IStrategy { }
[GetAll]
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
{
private IInventoryModel _model = null!;
private IReadOnlyList<IInventoryModel> _models = null!;
private ICombatSystem _system = null!;
private IUiUtility _utility = null!;
private IStrategy _service = null!;
private Godot.Node _node = 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>();
_models = this.GetModels<global::TestApp.IInventoryModel>();
_system = this.GetSystem<global::TestApp.ICombatSystem>();
_utility = this.GetUtility<global::TestApp.IUiUtility>();
_service = this.GetService<global::TestApp.IStrategy>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_BattlePanel.ContextGet.g.cs", expected));
Assert.Pass();
}
[Test]
public async Task Generates_Bindings_For_IContextAware_Class()
{
var source = """
using System;
using GFramework.SourceGenerators.Abstractions.Rule;
namespace GFramework.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
public sealed class GetServiceAttribute : 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 GetService<T>(this object contextAware) => default!;
}
}
namespace TestApp
{
public interface IStrategy { }
public partial class StrategyHost : GFramework.Core.Abstractions.Rule.IContextAware
{
[GetService]
private IStrategy _strategy = null!;
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class StrategyHost
{
private void __InjectContextBindings_Generated()
{
_strategy = this.GetService<global::TestApp.IStrategy>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_StrategyHost.ContextGet.g.cs", expected));
Assert.Pass();
}
[Test]
public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware()
{
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.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
{
[GetModel]
private IInventoryModel _model = null!;
}
}
""";
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error)
.WithSpan(31, 30, 31, 45)
.WithArguments("InventoryPanel"));
await test.RunAsync();
Assert.Pass();
}
[Test]
public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList()
{
var source = """
using System;
using System.Collections.Generic;
using GFramework.SourceGenerators.Abstractions.Rule;
namespace GFramework.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
public sealed class GetModelsAttribute : 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 IReadOnlyList<T> GetModels<T>(this object contextAware) => default!;
}
}
namespace TestApp
{
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
{
[GetModels]
private List<IInventoryModel> _models = new();
}
}
""";
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error)
.WithSpan(40, 40, 40, 47)
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
await test.RunAsync();
Assert.Pass();
}
}