mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
1362 lines
42 KiB
C#
1362 lines
42 KiB
C#
// Copyright (c) 2025-2026 GeWuYou
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
using GFramework.Core.SourceGenerators.Rule;
|
||
using GFramework.SourceGenerators.Tests.Core;
|
||
|
||
namespace GFramework.SourceGenerators.Tests.Rule;
|
||
|
||
/// <summary>
|
||
/// 验证 <see cref="ContextGetGenerator" /> 在显式特性、<c>GetAll</c> 推断与诊断场景下的生成契约。
|
||
/// </summary>
|
||
[TestFixture]
|
||
public class ContextGetGeneratorTests
|
||
{
|
||
private const string InventoryPanelGeneratedFileName = "TestApp_InventoryPanel.ContextGet.g.cs";
|
||
private const string BattlePanelGeneratedFileName = "TestApp_BattlePanel.ContextGet.g.cs";
|
||
private const string GameplayHudGeneratedFileName = "TestApp_GameplayHud.ContextGet.g.cs";
|
||
private const string StrategyHostGeneratedFileName = "TestApp_StrategyHost.ContextGet.g.cs";
|
||
|
||
private const string ContextAwareAttributeClassSource = """
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string ContextAwareAttributeClassExpected = """
|
||
// <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>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string FullyQualifiedFieldAttributesSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
|
||
{
|
||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||
public sealed class ContextAwareAttribute : Attribute { }
|
||
|
||
[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 { }
|
||
|
||
[ContextAware]
|
||
public partial class InventoryPanel
|
||
{
|
||
[global::GFramework.Core.SourceGenerators.Abstractions.Rule.GetModel]
|
||
private IInventoryModel _model = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string FullyQualifiedFieldAttributesExpected = """
|
||
// <auto-generated />
|
||
#nullable enable
|
||
|
||
using GFramework.Core.Extensions;
|
||
|
||
namespace TestApp;
|
||
|
||
partial class InventoryPanel
|
||
{
|
||
private void __InjectContextBindings_Generated()
|
||
{
|
||
_model = this.GetModel<global::TestApp.IInventoryModel>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string InferredGetAllClassSource = """
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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!;
|
||
}
|
||
}
|
||
|
||
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 IReadOnlyList<IStrategy> _services = null!;
|
||
private Godot.Node _node = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string InferredGetAllClassExpected = """
|
||
// <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>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string ExplicitServiceGetAllClassSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string ExplicitServiceGetAllClassExpected = """
|
||
// <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>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string GeneratedInjectionMethodAlreadyExistsMarkupSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
|
||
{
|
||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||
public sealed class ContextAwareAttribute : Attribute { }
|
||
|
||
[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 { }
|
||
|
||
[ContextAware]
|
||
public partial class InventoryPanel
|
||
{
|
||
[GetModel]
|
||
private IInventoryModel _model = null!;
|
||
|
||
private void {|#0:__InjectContextBindings_Generated|}()
|
||
{
|
||
}
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string IgnoreConstFieldGetAllSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string IgnoreConstFieldGetAllExpected = """
|
||
// <auto-generated />
|
||
#nullable enable
|
||
|
||
using GFramework.Core.Extensions;
|
||
|
||
namespace TestApp;
|
||
|
||
partial class BattlePanel
|
||
{
|
||
private void __InjectContextBindings_Generated()
|
||
{
|
||
_model = this.GetModel<global::TestApp.IInventoryModel>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string ReadonlyInferredFieldGetAllMarkupSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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 {|#0:_model|} = null!;
|
||
private ICombatSystem _system = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string SkipInvalidGetAllFieldExpected = """
|
||
// <auto-generated />
|
||
#nullable enable
|
||
|
||
using GFramework.Core.Extensions;
|
||
|
||
namespace TestApp;
|
||
|
||
partial class BattlePanel
|
||
{
|
||
private void __InjectContextBindings_Generated()
|
||
{
|
||
_system = this.GetSystem<global::TestApp.ICombatSystem>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string StaticInferredFieldGetAllMarkupSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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 {|#0:_model|} = null!;
|
||
private ICombatSystem _system = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string SkipNullableServiceLikeFieldSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string SkipNullableServiceLikeFieldExpected = """
|
||
// <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>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string IContextAwareClassSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string IContextAwareClassExpected = """
|
||
// <auto-generated />
|
||
#nullable enable
|
||
|
||
using GFramework.Core.Extensions;
|
||
|
||
namespace TestApp;
|
||
|
||
partial class StrategyHost
|
||
{
|
||
private void __InjectContextBindings_Generated()
|
||
{
|
||
_strategy = this.GetService<global::TestApp.IStrategy>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
private const string NonContextAwareClassMarkupSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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 {|#0:_model|} = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string GetModelsFieldNotIReadOnlyListMarkupSource = """
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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> {|#0:_models|} = new();
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string ReadonlyExplicitGetModelFieldMarkupSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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 {|#0:_model|} = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string StaticExplicitGetModelFieldMarkupSource = """
|
||
using System;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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 {|#0:_model|} = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string GetModelsAssignableSource = """
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||
|
||
namespace GFramework.Core.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 IEnumerable<IInventoryModel> _models = null!;
|
||
}
|
||
}
|
||
""";
|
||
|
||
private const string GetModelsAssignableExpected = """
|
||
// <auto-generated />
|
||
#nullable enable
|
||
|
||
using GFramework.Core.Extensions;
|
||
|
||
namespace TestApp;
|
||
|
||
partial class InventoryPanel
|
||
{
|
||
private void __InjectContextBindings_Generated()
|
||
{
|
||
_models = this.GetModels<global::TestApp.IInventoryModel>();
|
||
}
|
||
}
|
||
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 验证 <c>[ContextAware]</c> 类上的显式字段特性会生成模型与服务绑定。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Generates_Bindings_For_ContextAwareAttribute_Class()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
ContextAwareAttributeClassSource,
|
||
InventoryPanelGeneratedFileName,
|
||
ContextAwareAttributeClassExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证字段使用 fully-qualified 特性名时仍能生成绑定。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Generates_Bindings_For_Fully_Qualified_Field_Attributes()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
FullyQualifiedFieldAttributesSource,
|
||
InventoryPanelGeneratedFileName,
|
||
FullyQualifiedFieldAttributesExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 <c>GetAll</c> 会仅为可推断的 model、models、system 与 utility 字段生成绑定。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Generates_Inferred_Bindings_For_GetAll_Class()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
InferredGetAllClassSource,
|
||
BattlePanelGeneratedFileName,
|
||
InferredGetAllClassExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 <c>GetAll</c> 与显式 <c>[GetService]</c> 可以组合生成绑定。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Generates_Explicit_Service_Binding_For_GetAll_Class()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
ExplicitServiceGetAllClassSource,
|
||
BattlePanelGeneratedFileName,
|
||
ExplicitServiceGetAllClassExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证目标类已声明注入方法时会报告冲突诊断。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
|
||
{
|
||
var source = MarkupTestSource.Parse(GeneratedInjectionMethodAlreadyExistsMarkupSource);
|
||
|
||
return VerifyDiagnosticAsync(
|
||
source,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_Common_Class_002",
|
||
DiagnosticSeverity.Error,
|
||
"InventoryPanel",
|
||
"__InjectContextBindings_Generated"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 <c>GetAll</c> 会忽略不可推断的常量字段且不报告诊断。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
IgnoreConstFieldGetAllSource,
|
||
BattlePanelGeneratedFileName,
|
||
IgnoreConstFieldGetAllExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证只读推断字段会被跳过并报告 warning,同时其他字段保持生成。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class()
|
||
{
|
||
var source = MarkupTestSource.Parse(ReadonlyInferredFieldGetAllMarkupSource);
|
||
|
||
return VerifyDiagnosticAndGeneratedSourceAsync(
|
||
source,
|
||
BattlePanelGeneratedFileName,
|
||
SkipInvalidGetAllFieldExpected,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_ContextGet_008",
|
||
DiagnosticSeverity.Warning,
|
||
"_model"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证静态推断字段会被跳过并报告 warning,同时其他字段保持生成。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class()
|
||
{
|
||
var source = MarkupTestSource.Parse(StaticInferredFieldGetAllMarkupSource);
|
||
|
||
return VerifyDiagnosticAndGeneratedSourceAsync(
|
||
source,
|
||
BattlePanelGeneratedFileName,
|
||
SkipInvalidGetAllFieldExpected,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_ContextGet_007",
|
||
DiagnosticSeverity.Warning,
|
||
"_model"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 nullable 的服务样字段不会被 <c>GetAll</c> 误判为可注入字段。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
SkipNullableServiceLikeFieldSource,
|
||
GameplayHudGeneratedFileName,
|
||
SkipNullableServiceLikeFieldExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证实现 <c>IContextAware</c> 的类型无需 <c>[ContextAware]</c> 也能生成显式绑定。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Generates_Bindings_For_IContextAware_Class()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
IContextAwareClassSource,
|
||
StrategyHostGeneratedFileName,
|
||
IContextAwareClassExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证缺少上下文感知契约的类型会报告错误诊断。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Reports_Diagnostic_When_Class_Is_Not_ContextAware()
|
||
{
|
||
var source = MarkupTestSource.Parse(NonContextAwareClassMarkupSource);
|
||
|
||
return VerifyDiagnosticAsync(
|
||
source,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_ContextGet_005",
|
||
DiagnosticSeverity.Error,
|
||
"InventoryPanel"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 <c>[GetModels]</c> 字段若不是可赋值自 <c>IReadOnlyList<T></c> 会报告错误。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList()
|
||
{
|
||
var source = MarkupTestSource.Parse(GetModelsFieldNotIReadOnlyListMarkupSource);
|
||
|
||
return VerifyDiagnosticAsync(
|
||
source,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_ContextGet_004",
|
||
DiagnosticSeverity.Error,
|
||
"_models",
|
||
"System.Collections.Generic.List<TestApp.IInventoryModel>",
|
||
"GetModels"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证显式 <c>[GetModel]</c> 作用于只读字段时会报告错误。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field()
|
||
{
|
||
var source = MarkupTestSource.Parse(ReadonlyExplicitGetModelFieldMarkupSource);
|
||
|
||
return VerifyDiagnosticAsync(
|
||
source,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_ContextGet_003",
|
||
DiagnosticSeverity.Error,
|
||
"_model"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证显式 <c>[GetModel]</c> 作用于静态字段时会报告错误。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field()
|
||
{
|
||
var source = MarkupTestSource.Parse(StaticExplicitGetModelFieldMarkupSource);
|
||
|
||
return VerifyDiagnosticAsync(
|
||
source,
|
||
CreateSingleSpanDiagnostic(
|
||
source,
|
||
"GF_ContextGet_002",
|
||
DiagnosticSeverity.Error,
|
||
"_model"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 <c>[GetModels]</c> 字段可以赋值到更宽的可枚举接口上。
|
||
/// </summary>
|
||
[Test]
|
||
public Task Generates_Bindings_For_GetModels_Field_Assignable_From_IReadOnlyList()
|
||
{
|
||
return VerifyGeneratedSourceAsync(
|
||
GetModelsAssignableSource,
|
||
InventoryPanelGeneratedFileName,
|
||
GetModelsAssignableExpected);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行单个生成源码断言,保持文件名与文本快照语义不变。
|
||
/// </summary>
|
||
/// <param name="source">测试输入源码。</param>
|
||
/// <param name="generatedFileName">期望生成文件名。</param>
|
||
/// <param name="expectedGeneratedSource">期望生成源码。</param>
|
||
/// <returns>表示测试执行的异步任务。</returns>
|
||
private static Task VerifyGeneratedSourceAsync(
|
||
string source,
|
||
string generatedFileName,
|
||
string expectedGeneratedSource)
|
||
{
|
||
return GeneratorTest<ContextGetGenerator>.RunAsync(
|
||
source,
|
||
(generatedFileName, expectedGeneratedSource));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行仅关注诊断输出的生成器测试。
|
||
/// </summary>
|
||
/// <param name="source">包含 markup span 的测试源码。</param>
|
||
/// <param name="expectedDiagnostic">期望诊断。</param>
|
||
/// <returns>表示测试执行的异步任务。</returns>
|
||
private static Task VerifyDiagnosticAsync(
|
||
MarkupTestSource source,
|
||
DiagnosticResult expectedDiagnostic)
|
||
{
|
||
var test = CreateGeneratorTest(source.Source);
|
||
test.ExpectedDiagnostics.Add(expectedDiagnostic);
|
||
return test.RunAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行同时断言诊断与部分生成输出的生成器测试。
|
||
/// </summary>
|
||
/// <param name="source">包含 markup span 的测试源码。</param>
|
||
/// <param name="generatedFileName">期望生成文件名。</param>
|
||
/// <param name="expectedGeneratedSource">期望生成源码。</param>
|
||
/// <param name="expectedDiagnostic">期望诊断。</param>
|
||
/// <returns>表示测试执行的异步任务。</returns>
|
||
private static Task VerifyDiagnosticAndGeneratedSourceAsync(
|
||
MarkupTestSource source,
|
||
string generatedFileName,
|
||
string expectedGeneratedSource,
|
||
DiagnosticResult expectedDiagnostic)
|
||
{
|
||
var test = CreateGeneratorTest(source.Source);
|
||
test.TestState.GeneratedSources.Add(
|
||
(typeof(ContextGetGenerator), generatedFileName, NormalizeLineEndings(expectedGeneratedSource)));
|
||
test.ExpectedDiagnostics.Add(expectedDiagnostic);
|
||
return test.RunAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为单一 markup span 场景构造诊断结果,统一保持定位键与参数组装方式。
|
||
/// </summary>
|
||
/// <param name="source">包含 markup span 的测试源码。</param>
|
||
/// <param name="diagnosticId">诊断 ID。</param>
|
||
/// <param name="severity">诊断严重级别。</param>
|
||
/// <param name="arguments">诊断参数。</param>
|
||
/// <returns>绑定到 markup key <c>0</c> 的期望诊断。</returns>
|
||
private static DiagnosticResult CreateSingleSpanDiagnostic(
|
||
MarkupTestSource source,
|
||
string diagnosticId,
|
||
DiagnosticSeverity severity,
|
||
params string[] arguments)
|
||
{
|
||
return source.WithSpan(
|
||
new DiagnosticResult(diagnosticId, severity),
|
||
"0")
|
||
.WithArguments(arguments);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建禁用 trace 诊断的通用源生成器测试实例,避免各场景重复样板配置。
|
||
/// </summary>
|
||
/// <param name="source">测试输入源码。</param>
|
||
/// <returns>配置完成的生成器测试对象。</returns>
|
||
private static CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier> CreateGeneratorTest(string source)
|
||
{
|
||
return new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||
{
|
||
TestState =
|
||
{
|
||
Sources = { source }
|
||
},
|
||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||
};
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
}
|