Merge pull request #147 from GeWuYou/feat/context-aware-injection-generator

Feat/context aware injection generator
This commit is contained in:
gewuyou 2026-03-28 13:11:57 +08:00 committed by GitHub
commit e692c721e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1908 additions and 43 deletions

View File

@ -5,9 +5,9 @@ using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility; using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures; using GFramework.Core.Architectures;
using GFramework.Core.Extensions;
using GFramework.Core.Ioc; using GFramework.Core.Ioc;
using GFramework.Core.Rule; using GFramework.Core.Rule;
using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Rule; namespace GFramework.Core.Tests.Rule;
@ -18,6 +18,11 @@ namespace GFramework.Core.Tests.Rule;
[TestFixture] [TestFixture]
public class ContextAwareServiceExtensionsTests public class ContextAwareServiceExtensionsTests
{ {
private MicrosoftDiContainer _container = null!;
private ArchitectureContext _context = null!;
private TestContextAware _contextAware = null!;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
@ -34,10 +39,6 @@ public class ContextAwareServiceExtensionsTests
_container.Clear(); _container.Clear();
} }
private TestContextAware _contextAware = null!;
private ArchitectureContext _context = null!;
private MicrosoftDiContainer _container = null!;
[Test] [Test]
public void GetService_Should_Return_Registered_Service() public void GetService_Should_Return_Registered_Service()
{ {
@ -53,6 +54,18 @@ public class ContextAwareServiceExtensionsTests
Assert.That(result, Is.SameAs(service)); Assert.That(result, Is.SameAs(service));
} }
[Test]
public void GetService_Should_Throw_When_Context_Returns_Null_Service()
{
// Arrange
var contextAware = new TestContextAware();
((IContextAware)contextAware).SetContext(new TestArchitectureContextV3());
// Act / Assert
Assert.That(() => contextAware.GetService<TestService>(),
Throws.InvalidOperationException.With.Message.Contains("Service"));
}
[Test] [Test]
public void GetSystem_Should_Return_Registered_System() public void GetSystem_Should_Return_Registered_System()
{ {
@ -68,6 +81,18 @@ public class ContextAwareServiceExtensionsTests
Assert.That(result, Is.SameAs(system)); Assert.That(result, Is.SameAs(system));
} }
[Test]
public void GetSystem_Should_Throw_When_Context_Returns_Null_System()
{
// Arrange
var contextAware = new TestContextAware();
((IContextAware)contextAware).SetContext(new TestArchitectureContextV3());
// Act / Assert
Assert.That(() => contextAware.GetSystem<TestSystem>(),
Throws.InvalidOperationException.With.Message.Contains("System"));
}
[Test] [Test]
public void GetModel_Should_Return_Registered_Model() public void GetModel_Should_Return_Registered_Model()
{ {
@ -83,6 +108,18 @@ public class ContextAwareServiceExtensionsTests
Assert.That(result, Is.SameAs(model)); Assert.That(result, Is.SameAs(model));
} }
[Test]
public void GetModel_Should_Throw_When_Context_Returns_Null_Model()
{
// Arrange
var contextAware = new TestContextAware();
((IContextAware)contextAware).SetContext(new TestArchitectureContextV3());
// Act / Assert
Assert.That(() => contextAware.GetModel<TestModel>(),
Throws.InvalidOperationException.With.Message.Contains("Model"));
}
[Test] [Test]
public void GetUtility_Should_Return_Registered_Utility() public void GetUtility_Should_Return_Registered_Utility()
{ {
@ -98,6 +135,18 @@ public class ContextAwareServiceExtensionsTests
Assert.That(result, Is.SameAs(utility)); Assert.That(result, Is.SameAs(utility));
} }
[Test]
public void GetUtility_Should_Throw_When_Context_Returns_Null_Utility()
{
// Arrange
var contextAware = new TestContextAware();
((IContextAware)contextAware).SetContext(new TestArchitectureContextV3());
// Act / Assert
Assert.That(() => contextAware.GetUtility<TestUtility>(),
Throws.InvalidOperationException.With.Message.Contains("Utility"));
}
[Test] [Test]
public void GetServices_Should_Return_All_Registered_Services() public void GetServices_Should_Return_All_Registered_Services()
{ {

View File

@ -1,3 +1,4 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Rule; using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Systems;
@ -18,13 +19,15 @@ public static class ContextAwareServiceExtensions
/// </summary> /// </summary>
/// <typeparam name="TService">要获取的服务类型</typeparam> /// <typeparam name="TService">要获取的服务类型</typeparam>
/// <param name="contextAware">实现 IContextAware 接口的上下文感知对象</param> /// <param name="contextAware">实现 IContextAware 接口的上下文感知对象</param>
/// <returns>指定类型的服务实例,如果未找到则返回 null</returns> /// <returns>指定类型的服务实例,如果未找到则抛出异常</returns>
/// <exception cref="ArgumentNullException">当 contextAware 参数为 null 时抛出</exception> /// <exception cref="ArgumentNullException">当 contextAware 参数为 null 时抛出</exception>
public static TService? GetService<TService>(this IContextAware contextAware) where TService : class /// <exception cref="InvalidOperationException">当指定服务未注册时抛出</exception>
public static TService GetService<TService>(this IContextAware contextAware) where TService : class
{ {
ArgumentNullException.ThrowIfNull(contextAware); ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext(); var context = contextAware.GetContext();
return context.GetService<TService>(); return GetRequiredComponent(context, static architectureContext => architectureContext.GetService<TService>(),
"Service");
} }
/// <summary> /// <summary>
@ -34,11 +37,13 @@ public static class ContextAwareServiceExtensions
/// <param name="contextAware">实现 IContextAware 接口的对象</param> /// <param name="contextAware">实现 IContextAware 接口的对象</param>
/// <returns>指定类型的系统实例</returns> /// <returns>指定类型的系统实例</returns>
/// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception> /// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception>
public static TSystem? GetSystem<TSystem>(this IContextAware contextAware) where TSystem : class, ISystem /// <exception cref="InvalidOperationException">当指定系统未注册时抛出</exception>
public static TSystem GetSystem<TSystem>(this IContextAware contextAware) where TSystem : class, ISystem
{ {
ArgumentNullException.ThrowIfNull(contextAware); ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext(); var context = contextAware.GetContext();
return context.GetSystem<TSystem>(); return GetRequiredComponent(context, static architectureContext => architectureContext.GetSystem<TSystem>(),
"System");
} }
/// <summary> /// <summary>
@ -48,11 +53,13 @@ public static class ContextAwareServiceExtensions
/// <param name="contextAware">实现 IContextAware 接口的对象</param> /// <param name="contextAware">实现 IContextAware 接口的对象</param>
/// <returns>指定类型的模型实例</returns> /// <returns>指定类型的模型实例</returns>
/// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception> /// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception>
public static TModel? GetModel<TModel>(this IContextAware contextAware) where TModel : class, IModel /// <exception cref="InvalidOperationException">当指定模型未注册时抛出</exception>
public static TModel GetModel<TModel>(this IContextAware contextAware) where TModel : class, IModel
{ {
ArgumentNullException.ThrowIfNull(contextAware); ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext(); var context = contextAware.GetContext();
return context.GetModel<TModel>(); return GetRequiredComponent(context, static architectureContext => architectureContext.GetModel<TModel>(),
"Model");
} }
/// <summary> /// <summary>
@ -62,11 +69,13 @@ public static class ContextAwareServiceExtensions
/// <param name="contextAware">实现 IContextAware 接口的对象</param> /// <param name="contextAware">实现 IContextAware 接口的对象</param>
/// <returns>指定类型的工具实例</returns> /// <returns>指定类型的工具实例</returns>
/// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception> /// <exception cref="ArgumentNullException">当 contextAware 为 null 时抛出</exception>
public static TUtility? GetUtility<TUtility>(this IContextAware contextAware) where TUtility : class, IUtility /// <exception cref="InvalidOperationException">当指定工具未注册时抛出</exception>
public static TUtility GetUtility<TUtility>(this IContextAware contextAware) where TUtility : class, IUtility
{ {
ArgumentNullException.ThrowIfNull(contextAware); ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext(); var context = contextAware.GetContext();
return context.GetUtility<TUtility>(); return GetRequiredComponent(context, static architectureContext => architectureContext.GetUtility<TUtility>(),
"Utility");
} }
#endregion #endregion
@ -193,5 +202,15 @@ public static class ContextAwareServiceExtensions
return context.GetUtilitiesByPriority<TUtility>(); return context.GetUtilitiesByPriority<TUtility>();
} }
private static TComponent GetRequiredComponent<TComponent>(IArchitectureContext context,
Func<IArchitectureContext, TComponent> resolver, string componentKind)
where TComponent : class
{
ArgumentNullException.ThrowIfNull(context);
var component = resolver(context);
return component ?? throw new InvalidOperationException($"{componentKind} {typeof(TComponent)} not registered");
}
#endregion #endregion
} }

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记类需要自动推断并注入上下文相关字段。
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class GetAllAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入单个模型实例。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetModelAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入模型集合。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetModelsAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入单个服务实例。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetServiceAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入服务集合。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetServicesAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入单个系统实例。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetSystemAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入系统集合。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetSystemsAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入工具集合。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetUtilitiesAttribute : Attribute
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.SourceGenerators.Abstractions.Rule;
/// <summary>
/// 标记字段需要自动注入单个工具实例。
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetUtilityAttribute : Attribute
{
}

View File

@ -1,5 +1,7 @@
using GFramework.SourceGenerators.Common.Info; using GFramework.SourceGenerators.Common.Info;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace GFramework.SourceGenerators.Common.Extensions; namespace GFramework.SourceGenerators.Common.Extensions;
@ -69,38 +71,50 @@ public static class INamedTypeSymbolExtensions
: $"where {tp.Name} : {string.Join(", ", parts)}"; : $"where {tp.Name} : {string.Join(", ", parts)}";
} }
/// <summary>
/// 判断类型的所有声明是否均带有 partial 关键字。
/// </summary>
/// <param name="symbol">要获取完整类名的命名类型符号</param> /// <param name="symbol">要获取完整类名的命名类型符号</param>
extension(INamedTypeSymbol symbol) /// <returns>如果所有声明均为 partial则返回 <c>true</c>。</returns>
public static bool AreAllDeclarationsPartial(this INamedTypeSymbol symbol)
{ {
/// <summary> return symbol.DeclaringSyntaxReferences
/// 获取命名类型符号的完整类名(包括嵌套类型名称) .Select(static reference => reference.GetSyntax())
/// </summary> .OfType<ClassDeclarationSyntax>()
/// <returns>完整的类名,格式为"外层类名.内层类名.当前类名"</returns> .All(static declaration =>
public string GetFullClassName() declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
}
/// <summary>
/// 获取命名类型符号的完整类名(包括嵌套类型名称)
/// </summary>
/// <param name="symbol">要获取完整类名的命名类型符号</param>
/// <returns>完整的类名,格式为"外层类名.内层类名.当前类名"</returns>
public static string GetFullClassName(this INamedTypeSymbol symbol)
{
var names = new Stack<string>();
var current = symbol;
// 遍历包含类型链,将所有类型名称压入栈中
while (current != null)
{ {
var names = new Stack<string>(); names.Push(current.Name);
var current = symbol; current = current.ContainingType;
// 遍历包含类型链,将所有类型名称压入栈中
while (current != null)
{
names.Push(current.Name);
current = current.ContainingType;
}
// 将栈中的名称用点号连接,形成完整的类名
return string.Join(".", names);
} }
/// <summary> // 将栈中的名称用点号连接,形成完整的类名
/// 获取命名类型符号的命名空间名称 return string.Join(".", names);
/// </summary> }
/// <returns>命名空间名称如果是全局命名空间则返回null</returns>
public string? GetNamespace() /// <summary>
{ /// 获取命名类型符号的命名空间名称
return symbol.ContainingNamespace.IsGlobalNamespace /// </summary>
? null /// <param name="symbol">要获取完整类名的命名类型符号</param>
: symbol.ContainingNamespace.ToDisplayString(); /// <returns>命名空间名称如果是全局命名空间则返回null</returns>
} public static string? GetNamespace(this INamedTypeSymbol symbol)
{
return symbol.ContainingNamespace.IsGlobalNamespace
? null
: symbol.ContainingNamespace.ToDisplayString();
} }
} }

View File

@ -0,0 +1,39 @@
using Microsoft.CodeAnalysis;
namespace GFramework.SourceGenerators.Common.Extensions;
/// <summary>
/// 提供 <see cref="ITypeSymbol" /> 的通用符号判断扩展。
/// </summary>
public static class ITypeSymbolExtensions
{
/// <summary>
/// 判断当前类型是否等于或实现/继承目标类型。
/// </summary>
/// <param name="typeSymbol">当前类型符号。</param>
/// <param name="targetType">目标类型符号。</param>
/// <returns>若等于、实现或继承则返回 <c>true</c>。</returns>
public static bool IsAssignableTo(
this ITypeSymbol typeSymbol,
INamedTypeSymbol? targetType)
{
if (targetType is null)
return false;
if (SymbolEqualityComparer.Default.Equals(typeSymbol, targetType))
return true;
if (typeSymbol is INamedTypeSymbol namedType)
{
if (namedType.AllInterfaces.Any(i =>
SymbolEqualityComparer.Default.Equals(i, targetType)))
return true;
for (var current = namedType.BaseType; current is not null; current = current.BaseType)
if (SymbolEqualityComparer.Default.Equals(current, targetType))
return true;
}
return false;
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace GFramework.SourceGenerators.Common.Info;
/// <summary>
/// 表示字段级生成器候选成员。
/// </summary>
/// <param name="Variable">字段变量语法节点。</param>
/// <param name="FieldSymbol">字段符号。</param>
public sealed record FieldCandidateInfo(
VariableDeclaratorSyntax Variable,
IFieldSymbol FieldSymbol
);

View File

@ -0,0 +1,14 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace GFramework.SourceGenerators.Common.Info;
/// <summary>
/// 表示类型级生成器候选成员。
/// </summary>
/// <param name="Declaration">类型声明语法节点。</param>
/// <param name="TypeSymbol">类型符号。</param>
public sealed record TypeCandidateInfo(
ClassDeclarationSyntax Declaration,
INamedTypeSymbol TypeSymbol
);

View File

@ -0,0 +1,20 @@
// IsExternalInit.cs
// This type is required to support init-only setters and record types
// when targeting netstandard2.0 or older frameworks.
#if !NET5_0_OR_GREATER
using System.ComponentModel;
// ReSharper disable CheckNamespace
namespace System.Runtime.CompilerServices;
/// <summary>
/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。
/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
#endif

View File

@ -0,0 +1,584 @@
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_Bindings_For_Fully_Qualified_Field_Attributes()
{
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.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.SourceGenerators.Abstractions.Rule.GetModel]
private IInventoryModel _model = 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>();
}
}
""";
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(40, 33, 40, 39)
.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(46, 39, 46, 46)
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
await test.RunAsync();
Assert.Pass();
}
[Test]
public async Task Generates_Bindings_For_GetModels_Field_Assignable_From_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 IEnumerable<IInventoryModel> _models = null!;
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class InventoryPanel
{
private void __InjectContextBindings_Generated()
{
_models = this.GetModels<global::TestApp.IInventoryModel>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_InventoryPanel.ContextGet.g.cs", expected));
Assert.Pass();
}
}

View File

@ -7,6 +7,12 @@
-----------------------|----------------------------------|----------|------------------------ -----------------------|----------------------------------|----------|------------------------
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
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_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic

View File

@ -0,0 +1,75 @@
using Microsoft.CodeAnalysis;
namespace GFramework.SourceGenerators.Diagnostics;
/// <summary>
/// 提供 Context Get 注入生成器相关诊断。
/// </summary>
public static class ContextGetDiagnostics
{
/// <summary>
/// 不支持在嵌套类中生成注入代码。
/// </summary>
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
"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",
DiagnosticSeverity.Error,
true);
/// <summary>
/// 带注入语义的字段不能是静态字段。
/// </summary>
public static readonly DiagnosticDescriptor StaticFieldNotSupported = new(
"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",
DiagnosticSeverity.Error,
true);
/// <summary>
/// 带注入语义的字段不能是只读字段。
/// </summary>
public static readonly DiagnosticDescriptor ReadOnlyFieldNotSupported = new(
"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",
DiagnosticSeverity.Error,
true);
/// <summary>
/// 字段类型与注入特性不匹配。
/// </summary>
public static readonly DiagnosticDescriptor InvalidBindingType = new(
"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",
DiagnosticSeverity.Error,
true);
/// <summary>
/// 使用 Context Get 注入的类型必须是上下文感知类型。
/// </summary>
public static readonly DiagnosticDescriptor ContextAwareTypeRequired = new(
"GF_ContextGet_005",
"Context-aware type is required",
"Class '{0}' must be context-aware to use generated context Get injection",
"GFramework.SourceGenerators.Rule",
DiagnosticSeverity.Error,
true);
/// <summary>
/// 一个字段不允许同时声明多个 Context Get 特性。
/// </summary>
public static readonly DiagnosticDescriptor MultipleBindingAttributesNotSupported = new(
"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",
DiagnosticSeverity.Error,
true);
}

View File

@ -32,6 +32,11 @@
<ProjectReference Include="..\$(AssemblyName).Common\$(AssemblyName).Common.csproj" PrivateAssets="all"/> <ProjectReference Include="..\$(AssemblyName).Common\$(AssemblyName).Common.csproj" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Include="..\GFramework.SourceGenerators.Common\Internals\IsExternalInit.cs"
Link="Internals\IsExternalInit.cs"/>
</ItemGroup>
<!-- ★关键:只把 Generator DLL 放进 analyzers --> <!-- ★关键:只把 Generator DLL 放进 analyzers -->
<ItemGroup> <ItemGroup>
<!-- Generator 本体 --> <!-- Generator 本体 -->

View File

@ -0,0 +1,46 @@
# GFramework.SourceGenerators
Core 侧通用源码生成器模块。
## Context Get 注入
当类本身是上下文感知类型时,可以通过字段特性生成一个手动调用的注入方法:
- `[GetService]`
- `[GetServices]`
- `[GetSystem]`
- `[GetSystems]`
- `[GetModel]`
- `[GetModels]`
- `[GetUtility]`
- `[GetUtilities]`
- `[GetAll]`
上下文感知类满足以下任一条件即可:
- 类上带有 `[ContextAware]`
- 继承 `ContextAwareBase`
- 实现 `IContextAware`
生成器会生成 `__InjectContextBindings_Generated()`,需要在合适的生命周期中手动调用。在 Godot 中通常放在 `_Ready()`
```csharp
using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class InventoryPanel
{
[GetModel]
private IInventoryModel _inventory = null!;
[GetServices]
private IReadOnlyList<IInventoryStrategy> _strategies = null!;
public override void _Ready()
{
__InjectContextBindings_Generated();
}
}
```
`[GetAll]` 作用于类本身,会自动扫描字段并推断对应的 `GetX` 调用;已显式标记字段的优先级更高。

View File

@ -0,0 +1,899 @@
using System.Collections.Immutable;
using System.Text;
using GFramework.SourceGenerators.Common.Constants;
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;
/// <summary>
/// 为上下文感知类生成 Core 上下文 Get 注入方法。
/// </summary>
[Generator]
public sealed class ContextGetGenerator : IIncrementalGenerator
{
private const string InjectionMethodName = "__InjectContextBindings_Generated";
private const string GetAllAttributeMetadataName =
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetAllAttribute";
private const string ContextAwareAttributeMetadataName =
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.ContextAwareAttribute";
private const string IContextAwareMetadataName =
$"{PathContests.CoreAbstractionsNamespace}.Rule.IContextAware";
private const string ContextAwareBaseMetadataName =
$"{PathContests.CoreNamespace}.Rule.ContextAwareBase";
private const string IModelMetadataName =
$"{PathContests.CoreAbstractionsNamespace}.Model.IModel";
private const string ISystemMetadataName =
$"{PathContests.CoreAbstractionsNamespace}.Systems.ISystem";
private const string IUtilityMetadataName =
$"{PathContests.CoreAbstractionsNamespace}.Utility.IUtility";
private const string IReadOnlyListMetadataName =
"System.Collections.Generic.IReadOnlyList`1";
private const string GodotNodeMetadataName = "Godot.Node";
private static readonly ImmutableArray<BindingDescriptor> BindingDescriptors =
[
new(
BindingKind.Service,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetServiceAttribute",
"GetService",
false),
new(
BindingKind.Services,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetServicesAttribute",
"GetServices",
true),
new(
BindingKind.System,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetSystemAttribute",
"GetSystem",
false),
new(
BindingKind.Systems,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetSystemsAttribute",
"GetSystems",
true),
new(
BindingKind.Model,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetModelAttribute",
"GetModel",
false),
new(
BindingKind.Models,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetModelsAttribute",
"GetModels",
true),
new(
BindingKind.Utility,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetUtilityAttribute",
"GetUtility",
false),
new(
BindingKind.Utilities,
$"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetUtilitiesAttribute",
"GetUtilities",
true)
];
private static readonly ImmutableHashSet<string> FieldCandidateAttributeNames = BindingDescriptors
.SelectMany(static descriptor => new[]
{
descriptor.AttributeName,
descriptor.AttributeName + "Attribute"
})
.ToImmutableHashSet(StringComparer.Ordinal);
private static readonly ImmutableHashSet<string> TypeCandidateAttributeNames =
[
"GetAll",
"GetAllAttribute"
];
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var fieldCandidates = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => IsFieldCandidate(node),
static (ctx, _) => TransformField(ctx))
.Where(static candidate => candidate is not null)
.Collect();
var typeCandidates = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => IsTypeCandidate(node),
static (ctx, _) => TransformType(ctx))
.Where(static candidate => candidate is not null)
.Collect();
var compilationAndFields = context.CompilationProvider.Combine(fieldCandidates);
var generationInput = compilationAndFields.Combine(typeCandidates);
context.RegisterSourceOutput(generationInput,
static (spc, pair) => Execute(
spc,
pair.Left.Left,
pair.Left.Right,
pair.Right));
}
private static bool IsFieldCandidate(SyntaxNode node)
{
if (node is not VariableDeclaratorSyntax
{
Parent: VariableDeclarationSyntax
{
Parent: FieldDeclarationSyntax fieldDeclaration
}
})
return false;
return HasCandidateAttribute(fieldDeclaration.AttributeLists, FieldCandidateAttributeNames);
}
private static FieldCandidateInfo? TransformField(GeneratorSyntaxContext context)
{
if (context.Node is not VariableDeclaratorSyntax variable)
return null;
if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol)
return null;
return HasAnyBindingAttribute(fieldSymbol, context.SemanticModel.Compilation)
? new FieldCandidateInfo(variable, fieldSymbol)
: null;
}
private static bool IsTypeCandidate(SyntaxNode node)
{
if (node is not ClassDeclarationSyntax classDeclaration)
return false;
return HasCandidateAttribute(classDeclaration.AttributeLists, TypeCandidateAttributeNames);
}
private static TypeCandidateInfo? TransformType(GeneratorSyntaxContext context)
{
if (context.Node is not ClassDeclarationSyntax classDeclaration)
return null;
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
return null;
return HasAttribute(typeSymbol, context.SemanticModel.Compilation, GetAllAttributeMetadataName)
? new TypeCandidateInfo(classDeclaration, typeSymbol)
: null;
}
private static void Execute(
SourceProductionContext context,
Compilation compilation,
ImmutableArray<FieldCandidateInfo?> fieldCandidates,
ImmutableArray<TypeCandidateInfo?> typeCandidates)
{
if (fieldCandidates.IsDefaultOrEmpty && typeCandidates.IsDefaultOrEmpty)
return;
var descriptors = ResolveBindingDescriptors(compilation);
var getAllAttribute = compilation.GetTypeByMetadataName(GetAllAttributeMetadataName);
if (descriptors.Length == 0 && getAllAttribute is null)
return;
var symbols = CreateContextSymbols(compilation);
var workItems = CollectWorkItems(
fieldCandidates,
typeCandidates,
descriptors,
getAllAttribute);
GenerateSources(context, descriptors, symbols, workItems);
}
private static ContextSymbols CreateContextSymbols(Compilation compilation)
{
return new ContextSymbols(
compilation.GetTypeByMetadataName(ContextAwareAttributeMetadataName),
compilation.GetTypeByMetadataName(IContextAwareMetadataName),
compilation.GetTypeByMetadataName(ContextAwareBaseMetadataName),
compilation.GetTypeByMetadataName(IModelMetadataName),
compilation.GetTypeByMetadataName(ISystemMetadataName),
compilation.GetTypeByMetadataName(IUtilityMetadataName),
compilation.GetTypeByMetadataName(IReadOnlyListMetadataName),
compilation.GetTypeByMetadataName(GodotNodeMetadataName));
}
private static void GenerateSources(
SourceProductionContext context,
ImmutableArray<ResolvedBindingDescriptor> descriptors,
ContextSymbols symbols,
Dictionary<INamedTypeSymbol, TypeWorkItem> workItems)
{
foreach (var workItem in workItems.Values)
{
if (!CanGenerateForType(context, workItem, symbols))
continue;
var bindings = CollectBindings(context, workItem, descriptors, symbols);
if (bindings.Count == 0 && workItem.GetAllDeclaration is null)
continue;
var source = GenerateSource(workItem.TypeSymbol, bindings);
context.AddSource(GetHintName(workItem.TypeSymbol), source);
}
}
private static List<BindingInfo> CollectBindings(
SourceProductionContext context,
TypeWorkItem workItem,
ImmutableArray<ResolvedBindingDescriptor> descriptors,
ContextSymbols symbols)
{
var bindings = new List<BindingInfo>();
var explicitFields = new HashSet<IFieldSymbol>(SymbolEqualityComparer.Default);
AddExplicitBindings(context, workItem, descriptors, symbols, bindings, explicitFields);
AddInferredBindings(context, workItem, symbols, bindings, explicitFields);
return bindings;
}
private static void AddExplicitBindings(
SourceProductionContext context,
TypeWorkItem workItem,
ImmutableArray<ResolvedBindingDescriptor> descriptors,
ContextSymbols symbols,
ICollection<BindingInfo> bindings,
ISet<IFieldSymbol> explicitFields)
{
foreach (var candidate in workItem.FieldCandidates
.OrderBy(static candidate => candidate.Variable.SpanStart)
.ThenBy(static candidate => candidate.FieldSymbol.Name, StringComparer.Ordinal))
{
var matches = ResolveExplicitBindings(candidate.FieldSymbol, descriptors);
if (matches.Length == 0)
continue;
explicitFields.Add(candidate.FieldSymbol);
if (matches.Length > 1)
{
ReportFieldDiagnostic(
context,
ContextGetDiagnostics.MultipleBindingAttributesNotSupported,
candidate);
continue;
}
if (!TryCreateExplicitBinding(
context,
candidate,
matches[0],
symbols,
out var binding))
continue;
bindings.Add(binding);
}
}
private static void AddInferredBindings(
SourceProductionContext context,
TypeWorkItem workItem,
ContextSymbols symbols,
ICollection<BindingInfo> bindings,
ISet<IFieldSymbol> explicitFields)
{
if (workItem.GetAllDeclaration is null)
return;
foreach (var field in GetAllFields(workItem.TypeSymbol))
{
if (explicitFields.Contains(field))
continue;
if (!CanInferBinding(context, field))
continue;
if (!TryCreateInferredBinding(field, symbols, out var binding))
continue;
bindings.Add(binding);
}
}
private static bool CanInferBinding(SourceProductionContext context, IFieldSymbol field)
{
if (field.IsStatic)
{
ReportFieldDiagnostic(
context,
ContextGetDiagnostics.StaticFieldNotSupported,
field);
return false;
}
if (!field.IsReadOnly)
return true;
ReportFieldDiagnostic(
context,
ContextGetDiagnostics.ReadOnlyFieldNotSupported,
field);
return false;
}
private static bool HasCandidateAttribute(
SyntaxList<AttributeListSyntax> attributeLists,
ImmutableHashSet<string> candidateNames)
{
return attributeLists
.SelectMany(static list => list.Attributes)
.Any(attribute => TryGetAttributeSimpleName(attribute.Name, out var name) && candidateNames.Contains(name));
}
private static bool TryGetAttributeSimpleName(NameSyntax attributeName, out string name)
{
switch (attributeName)
{
case SimpleNameSyntax simpleName:
name = simpleName.Identifier.ValueText;
return true;
case QualifiedNameSyntax qualifiedName:
name = qualifiedName.Right.Identifier.ValueText;
return true;
case AliasQualifiedNameSyntax aliasQualifiedName:
name = aliasQualifiedName.Name.Identifier.ValueText;
return true;
default:
name = string.Empty;
return false;
}
}
private static bool HasAnyBindingAttribute(IFieldSymbol fieldSymbol, Compilation compilation)
{
return Enumerable.Any(BindingDescriptors,
descriptor => HasAttribute(fieldSymbol, compilation, descriptor.MetadataName));
}
private static bool HasAttribute(
ISymbol symbol,
Compilation compilation,
string metadataName)
{
var attributeSymbol = compilation.GetTypeByMetadataName(metadataName);
return attributeSymbol is not null &&
symbol.GetAttributes().Any(attribute =>
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeSymbol));
}
private static Dictionary<INamedTypeSymbol, TypeWorkItem> CollectWorkItems(
ImmutableArray<FieldCandidateInfo?> fieldCandidates,
ImmutableArray<TypeCandidateInfo?> typeCandidates,
ImmutableArray<ResolvedBindingDescriptor> descriptors,
INamedTypeSymbol? getAllAttribute)
{
var workItems = new Dictionary<INamedTypeSymbol, TypeWorkItem>(SymbolEqualityComparer.Default);
foreach (var candidate in fieldCandidates
.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!))
{
if (ResolveExplicitBindings(candidate.FieldSymbol, descriptors).Length == 0)
continue;
var typeSymbol = candidate.FieldSymbol.ContainingType;
if (!workItems.TryGetValue(typeSymbol, out var workItem))
{
workItem = new TypeWorkItem(typeSymbol);
workItems.Add(typeSymbol, workItem);
}
workItem.FieldCandidates.Add(candidate);
}
if (getAllAttribute is null)
return workItems;
foreach (var candidate in typeCandidates
.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!))
{
if (!candidate.TypeSymbol.GetAttributes().Any(attribute =>
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, getAllAttribute)))
continue;
if (!workItems.TryGetValue(candidate.TypeSymbol, out var workItem))
{
workItem = new TypeWorkItem(candidate.TypeSymbol);
workItems.Add(candidate.TypeSymbol, workItem);
}
workItem.GetAllDeclaration ??= candidate.Declaration;
}
return workItems;
}
private static bool CanGenerateForType(
SourceProductionContext context,
TypeWorkItem workItem,
ContextSymbols symbols)
{
if (workItem.TypeSymbol.ContainingType is not null)
{
context.ReportDiagnostic(Diagnostic.Create(
ContextGetDiagnostics.NestedClassNotSupported,
GetTypeLocation(workItem),
workItem.TypeSymbol.Name));
return false;
}
if (!workItem.TypeSymbol.AreAllDeclarationsPartial())
{
context.ReportDiagnostic(Diagnostic.Create(
CommonDiagnostics.ClassMustBePartial,
GetTypeLocation(workItem),
workItem.TypeSymbol.Name));
return false;
}
if (IsContextAwareType(workItem.TypeSymbol, symbols))
return true;
context.ReportDiagnostic(Diagnostic.Create(
ContextGetDiagnostics.ContextAwareTypeRequired,
GetTypeLocation(workItem),
workItem.TypeSymbol.Name));
return false;
}
private static bool IsContextAwareType(
INamedTypeSymbol typeSymbol,
ContextSymbols symbols)
{
if (symbols.ContextAwareAttribute is not null &&
typeSymbol.GetAttributes().Any(attribute =>
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbols.ContextAwareAttribute)))
return true;
return typeSymbol.IsAssignableTo(symbols.IContextAware) ||
typeSymbol.IsAssignableTo(symbols.ContextAwareBase);
}
private static ImmutableArray<ResolvedBindingDescriptor> ResolveBindingDescriptors(Compilation compilation)
{
var builder = ImmutableArray.CreateBuilder<ResolvedBindingDescriptor>(BindingDescriptors.Length);
foreach (var descriptor in BindingDescriptors)
{
var attributeSymbol = compilation.GetTypeByMetadataName(descriptor.MetadataName);
if (attributeSymbol is null)
continue;
builder.Add(new ResolvedBindingDescriptor(descriptor, attributeSymbol));
}
return builder.ToImmutable();
}
private static ImmutableArray<ResolvedBindingDescriptor> ResolveExplicitBindings(
IFieldSymbol fieldSymbol,
ImmutableArray<ResolvedBindingDescriptor> descriptors)
{
if (descriptors.IsDefaultOrEmpty)
return [];
var builder = ImmutableArray.CreateBuilder<ResolvedBindingDescriptor>();
foreach (var descriptor in descriptors.Where(descriptor => fieldSymbol.GetAttributes().Any(attribute =>
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, descriptor.AttributeSymbol))))
{
builder.Add(descriptor);
}
return builder.ToImmutable();
}
private static bool TryCreateExplicitBinding(
SourceProductionContext context,
FieldCandidateInfo candidate,
ResolvedBindingDescriptor descriptor,
ContextSymbols symbols,
out BindingInfo binding)
{
binding = default;
if (candidate.FieldSymbol.IsStatic)
{
ReportFieldDiagnostic(
context,
ContextGetDiagnostics.StaticFieldNotSupported,
candidate);
return false;
}
if (candidate.FieldSymbol.IsReadOnly)
{
ReportFieldDiagnostic(
context,
ContextGetDiagnostics.ReadOnlyFieldNotSupported,
candidate);
return false;
}
if (!TryResolveBindingTarget(candidate.FieldSymbol.Type, descriptor.Definition.Kind, symbols,
out var targetType))
{
context.ReportDiagnostic(Diagnostic.Create(
ContextGetDiagnostics.InvalidBindingType,
candidate.Variable.Identifier.GetLocation(),
candidate.FieldSymbol.Name,
candidate.FieldSymbol.Type.ToDisplayString(),
descriptor.Definition.AttributeName));
return false;
}
binding = new BindingInfo(candidate.FieldSymbol, descriptor.Definition.Kind, targetType);
return true;
}
private static bool TryCreateInferredBinding(
IFieldSymbol fieldSymbol,
ContextSymbols symbols,
out BindingInfo binding)
{
binding = default;
if (symbols.GodotNode is not null && fieldSymbol.Type.IsAssignableTo(symbols.GodotNode))
return false;
if (TryResolveCollectionElement(fieldSymbol.Type, symbols.IReadOnlyList, out var elementType))
{
if (elementType.IsAssignableTo(symbols.IModel))
{
binding = new BindingInfo(fieldSymbol, BindingKind.Models, elementType);
return true;
}
if (elementType.IsAssignableTo(symbols.ISystem))
{
binding = new BindingInfo(fieldSymbol, BindingKind.Systems, elementType);
return true;
}
if (elementType.IsAssignableTo(symbols.IUtility))
{
binding = new BindingInfo(fieldSymbol, BindingKind.Utilities, elementType);
return true;
}
if (elementType.IsReferenceType)
{
binding = new BindingInfo(fieldSymbol, BindingKind.Services, elementType);
return true;
}
return false;
}
if (fieldSymbol.Type.IsAssignableTo(symbols.IModel))
{
binding = new BindingInfo(fieldSymbol, BindingKind.Model, fieldSymbol.Type);
return true;
}
if (fieldSymbol.Type.IsAssignableTo(symbols.ISystem))
{
binding = new BindingInfo(fieldSymbol, BindingKind.System, fieldSymbol.Type);
return true;
}
if (fieldSymbol.Type.IsAssignableTo(symbols.IUtility))
{
binding = new BindingInfo(fieldSymbol, BindingKind.Utility, fieldSymbol.Type);
return true;
}
if (fieldSymbol.Type.IsReferenceType)
{
binding = new BindingInfo(fieldSymbol, BindingKind.Service, fieldSymbol.Type);
return true;
}
return false;
}
private static bool TryResolveBindingTarget(
ITypeSymbol fieldType,
BindingKind kind,
ContextSymbols symbols,
out ITypeSymbol targetType)
{
targetType = null!;
switch (kind)
{
case BindingKind.Service:
if (!fieldType.IsReferenceType)
return false;
targetType = fieldType;
return true;
case BindingKind.Model:
if (!fieldType.IsAssignableTo(symbols.IModel))
return false;
targetType = fieldType;
return true;
case BindingKind.System:
if (!fieldType.IsAssignableTo(symbols.ISystem))
return false;
targetType = fieldType;
return true;
case BindingKind.Utility:
if (!fieldType.IsAssignableTo(symbols.IUtility))
return false;
targetType = fieldType;
return true;
case BindingKind.Services:
return TryResolveReferenceCollection(fieldType, symbols.IReadOnlyList, out targetType);
case BindingKind.Models:
return TryResolveConstrainedCollection(fieldType, symbols.IReadOnlyList, symbols.IModel,
out targetType);
case BindingKind.Systems:
return TryResolveConstrainedCollection(fieldType, symbols.IReadOnlyList, symbols.ISystem,
out targetType);
case BindingKind.Utilities:
return TryResolveConstrainedCollection(fieldType, symbols.IReadOnlyList, symbols.IUtility,
out targetType);
default:
return false;
}
}
private static bool TryResolveReferenceCollection(
ITypeSymbol fieldType,
INamedTypeSymbol? readOnlyList,
out ITypeSymbol elementType)
{
elementType = null!;
if (!TryResolveCollectionElement(fieldType, readOnlyList, out var candidate))
return false;
if (!candidate.IsReferenceType)
return false;
elementType = candidate;
return true;
}
private static bool TryResolveConstrainedCollection(
ITypeSymbol fieldType,
INamedTypeSymbol? readOnlyList,
INamedTypeSymbol? constraintType,
out ITypeSymbol elementType)
{
elementType = null!;
if (!TryResolveCollectionElement(fieldType, readOnlyList, out var candidate))
return false;
if (!candidate.IsAssignableTo(constraintType))
return false;
elementType = candidate;
return true;
}
private static bool TryResolveCollectionElement(
ITypeSymbol fieldType,
INamedTypeSymbol? readOnlyList,
out ITypeSymbol elementType)
{
elementType = null!;
if (readOnlyList is null || fieldType is not INamedTypeSymbol targetType)
return false;
foreach (var candidateType in EnumerateCollectionTypeCandidates(targetType))
{
if (candidateType.TypeArguments.Length != 1)
continue;
var candidateElementType = candidateType.TypeArguments[0];
var expectedSourceType = readOnlyList.Construct(candidateElementType);
if (!expectedSourceType.IsAssignableTo(targetType))
continue;
elementType = candidateElementType;
return true;
}
return false;
}
private static IEnumerable<INamedTypeSymbol> EnumerateCollectionTypeCandidates(INamedTypeSymbol typeSymbol)
{
yield return typeSymbol;
foreach (var interfaceType in typeSymbol.AllInterfaces)
yield return interfaceType;
}
private static IEnumerable<IFieldSymbol> GetAllFields(INamedTypeSymbol typeSymbol)
{
return typeSymbol.GetMembers()
.OfType<IFieldSymbol>()
.Where(static field => !field.IsImplicitlyDeclared)
.OrderBy(static field => field.Locations.FirstOrDefault()?.SourceSpan.Start ?? int.MaxValue)
.ThenBy(static field => field.Name, StringComparer.Ordinal);
}
private static void ReportFieldDiagnostic(
SourceProductionContext context,
DiagnosticDescriptor descriptor,
FieldCandidateInfo candidate)
{
context.ReportDiagnostic(Diagnostic.Create(
descriptor,
candidate.Variable.Identifier.GetLocation(),
candidate.FieldSymbol.Name));
}
private static void ReportFieldDiagnostic(
SourceProductionContext context,
DiagnosticDescriptor descriptor,
IFieldSymbol fieldSymbol)
{
context.ReportDiagnostic(Diagnostic.Create(
descriptor,
fieldSymbol.Locations.FirstOrDefault() ?? Location.None,
fieldSymbol.Name));
}
private static Location GetTypeLocation(TypeWorkItem workItem)
{
if (workItem.GetAllDeclaration is not null)
return workItem.GetAllDeclaration.Identifier.GetLocation();
return workItem.FieldCandidates[0].Variable.Identifier.GetLocation();
}
private static string GenerateSource(
INamedTypeSymbol typeSymbol,
IReadOnlyList<BindingInfo> bindings)
{
var namespaceName = typeSymbol.GetNamespace();
var generics = typeSymbol.ResolveGenerics();
var orderedBindings = bindings
.OrderBy(static binding => binding.Field.Locations.FirstOrDefault()?.SourceSpan.Start ?? int.MaxValue)
.ThenBy(static binding => binding.Field.Name, StringComparer.Ordinal)
.ToList();
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine();
sb.AppendLine("using GFramework.Core.Extensions;");
sb.AppendLine();
if (namespaceName is not null)
{
sb.AppendLine($"namespace {namespaceName};");
sb.AppendLine();
}
sb.AppendLine($"partial class {typeSymbol.Name}{generics.Parameters}");
foreach (var constraint in generics.Constraints)
sb.AppendLine($" {constraint}");
sb.AppendLine("{");
sb.AppendLine($" private void {InjectionMethodName}()");
sb.AppendLine(" {");
foreach (var binding in orderedBindings)
{
var targetType = binding.TargetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
sb.AppendLine($" {binding.Field.Name} = {ResolveAccessor(binding.Kind, targetType)};");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string ResolveAccessor(BindingKind kind, string targetType)
{
return kind switch
{
BindingKind.Service => $"this.GetService<{targetType}>()",
BindingKind.Services => $"this.GetServices<{targetType}>()",
BindingKind.System => $"this.GetSystem<{targetType}>()",
BindingKind.Systems => $"this.GetSystems<{targetType}>()",
BindingKind.Model => $"this.GetModel<{targetType}>()",
BindingKind.Models => $"this.GetModels<{targetType}>()",
BindingKind.Utility => $"this.GetUtility<{targetType}>()",
BindingKind.Utilities => $"this.GetUtilities<{targetType}>()",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
};
}
private static string GetHintName(INamedTypeSymbol typeSymbol)
{
var hintName = typeSymbol.GetNamespace() is { Length: > 0 } namespaceName
? $"{namespaceName}.{typeSymbol.GetFullClassName()}"
: typeSymbol.GetFullClassName();
return hintName.Replace('.', '_') + ".ContextGet.g.cs";
}
private enum BindingKind
{
Service,
Services,
System,
Systems,
Model,
Models,
Utility,
Utilities
}
private sealed record BindingDescriptor(
BindingKind Kind,
string MetadataName,
string AttributeName,
bool IsCollection);
private readonly record struct ResolvedBindingDescriptor(
BindingDescriptor Definition,
INamedTypeSymbol AttributeSymbol);
private readonly record struct BindingInfo(
IFieldSymbol Field,
BindingKind Kind,
ITypeSymbol TargetType);
private readonly record struct ContextSymbols(
INamedTypeSymbol? ContextAwareAttribute,
INamedTypeSymbol? IContextAware,
INamedTypeSymbol? ContextAwareBase,
INamedTypeSymbol? IModel,
INamedTypeSymbol? ISystem,
INamedTypeSymbol? IUtility,
INamedTypeSymbol? IReadOnlyList,
INamedTypeSymbol? GodotNode);
private sealed class TypeWorkItem(INamedTypeSymbol typeSymbol)
{
public INamedTypeSymbol TypeSymbol { get; } = typeSymbol;
public List<FieldCandidateInfo> FieldCandidates { get; } = [];
public ClassDeclarationSyntax? GetAllDeclaration { get; set; }
}
}