mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-28 16:13:28 +08:00
feat(generator): 添加上下文感知注入源码生成器
- 移除ContextAwareServiceExtensions中GetService/GetSystem/GetModel/GetUtility方法的可空返回值 - 添加ContextGetGenerator源码生成器,支持通过特性自动生成上下文注入代码 - 新增GetService/GetServices/GetSystem/GetSystems/GetModel/GetModels/GetUtility/GetUtilities/GetAll特性 - 添加ContextGetDiagnostics提供注入相关的编译时诊断检查 - 实现INamedTypeSymbol扩展方法AreAllDeclarationsPartial用于检查partial类声明 - 添加ITypeSymbol扩展方法IsAssignableTo用于类型兼容性判断 - 创建FieldCandidateInfo和TypeCandidateInfo记录类型用于存储生成器候选信息 - 添加IsExternalInit内部类型支持低版本.NET框架的init-only setter功能 - 更新AnalyzerReleases.Unshipped.md添加新的诊断规则条目 - 创建完整的单元测试验证生成器功能和各种边界情况
This commit is contained in:
parent
c681c4861f
commit
78af119f38
@ -20,7 +20,7 @@ public static class ContextAwareServiceExtensions
|
|||||||
/// <param name="contextAware">实现 IContextAware 接口的上下文感知对象</param>
|
/// <param name="contextAware">实现 IContextAware 接口的上下文感知对象</param>
|
||||||
/// <returns>指定类型的服务实例,如果未找到则返回 null</returns>
|
/// <returns>指定类型的服务实例,如果未找到则返回 null</returns>
|
||||||
/// <exception cref="ArgumentNullException">当 contextAware 参数为 null 时抛出</exception>
|
/// <exception cref="ArgumentNullException">当 contextAware 参数为 null 时抛出</exception>
|
||||||
public static TService? GetService<TService>(this IContextAware contextAware) where TService : class
|
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();
|
||||||
@ -34,7 +34,7 @@ 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
|
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();
|
||||||
@ -48,7 +48,7 @@ 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
|
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();
|
||||||
@ -62,7 +62,7 @@ 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
|
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();
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记类需要自动推断并注入上下文相关字段。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetAllAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入单个模型实例。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetModelAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入模型集合。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetModelsAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入单个服务实例。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetServiceAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入服务集合。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetServicesAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入单个系统实例。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetSystemAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入系统集合。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetSystemsAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入工具集合。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetUtilitiesAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记字段需要自动注入单个工具实例。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetUtilityAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
);
|
||||||
14
GFramework.SourceGenerators.Common/Info/TypeCandidateInfo.cs
Normal file
14
GFramework.SourceGenerators.Common/Info/TypeCandidateInfo.cs
Normal 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
|
||||||
|
);
|
||||||
@ -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
|
||||||
@ -0,0 +1,425 @@
|
|||||||
|
using GFramework.SourceGenerators.Rule;
|
||||||
|
using GFramework.SourceGenerators.Tests.Core;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||||
|
using Microsoft.CodeAnalysis.Testing;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Tests.Rule;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ContextGetGeneratorTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task Generates_Bindings_For_ContextAwareAttribute_Class()
|
||||||
|
{
|
||||||
|
var source = """
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public sealed class ContextAwareAttribute : Attribute { }
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||||
|
public sealed class GetModelAttribute : Attribute { }
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||||
|
public sealed class GetServicesAttribute : Attribute { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Rule
|
||||||
|
{
|
||||||
|
public interface IContextAware { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Model
|
||||||
|
{
|
||||||
|
public interface IModel { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Systems
|
||||||
|
{
|
||||||
|
public interface ISystem { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Utility
|
||||||
|
{
|
||||||
|
public interface IUtility { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class ContextAwareServiceExtensions
|
||||||
|
{
|
||||||
|
public static T GetModel<T>(this object contextAware) => default!;
|
||||||
|
public static IReadOnlyList<T> GetServices<T>(this object contextAware) => default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||||
|
public interface IInventoryStrategy { }
|
||||||
|
|
||||||
|
[ContextAware]
|
||||||
|
public partial class InventoryPanel
|
||||||
|
{
|
||||||
|
[GetModel]
|
||||||
|
private IInventoryModel _model = null!;
|
||||||
|
|
||||||
|
[GetServices]
|
||||||
|
private IReadOnlyList<IInventoryStrategy> _strategies = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string expected = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using GFramework.Core.Extensions;
|
||||||
|
|
||||||
|
namespace TestApp;
|
||||||
|
|
||||||
|
partial class InventoryPanel
|
||||||
|
{
|
||||||
|
private void __InjectContextBindings_Generated()
|
||||||
|
{
|
||||||
|
_model = this.GetModel<global::TestApp.IInventoryModel>();
|
||||||
|
_strategies = this.GetServices<global::TestApp.IInventoryStrategy>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
await GeneratorTest<ContextGetGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("TestApp_InventoryPanel.ContextGet.g.cs", expected));
|
||||||
|
Assert.Pass();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Generates_Inferred_Bindings_For_GetAll_Class()
|
||||||
|
{
|
||||||
|
var source = """
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public sealed class GetAllAttribute : Attribute { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Rule
|
||||||
|
{
|
||||||
|
public interface IContextAware { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Architectures
|
||||||
|
{
|
||||||
|
public interface IArchitectureContext { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Rule
|
||||||
|
{
|
||||||
|
public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Model
|
||||||
|
{
|
||||||
|
public interface IModel { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Systems
|
||||||
|
{
|
||||||
|
public interface ISystem { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Utility
|
||||||
|
{
|
||||||
|
public interface IUtility { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class ContextAwareServiceExtensions
|
||||||
|
{
|
||||||
|
public static T GetModel<T>(this object contextAware) => default!;
|
||||||
|
public static IReadOnlyList<T> GetModels<T>(this object contextAware) => default!;
|
||||||
|
public static T GetSystem<T>(this object contextAware) => default!;
|
||||||
|
public static T GetUtility<T>(this object contextAware) => default!;
|
||||||
|
public static T GetService<T>(this object contextAware) => default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Godot
|
||||||
|
{
|
||||||
|
public class Node { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||||
|
public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { }
|
||||||
|
public interface IUiUtility : GFramework.Core.Abstractions.Utility.IUtility { }
|
||||||
|
public interface IStrategy { }
|
||||||
|
|
||||||
|
[GetAll]
|
||||||
|
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
||||||
|
{
|
||||||
|
private IInventoryModel _model = null!;
|
||||||
|
private IReadOnlyList<IInventoryModel> _models = null!;
|
||||||
|
private ICombatSystem _system = null!;
|
||||||
|
private IUiUtility _utility = null!;
|
||||||
|
private IStrategy _service = null!;
|
||||||
|
private Godot.Node _node = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string expected = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using GFramework.Core.Extensions;
|
||||||
|
|
||||||
|
namespace TestApp;
|
||||||
|
|
||||||
|
partial class BattlePanel
|
||||||
|
{
|
||||||
|
private void __InjectContextBindings_Generated()
|
||||||
|
{
|
||||||
|
_model = this.GetModel<global::TestApp.IInventoryModel>();
|
||||||
|
_models = this.GetModels<global::TestApp.IInventoryModel>();
|
||||||
|
_system = this.GetSystem<global::TestApp.ICombatSystem>();
|
||||||
|
_utility = this.GetUtility<global::TestApp.IUiUtility>();
|
||||||
|
_service = this.GetService<global::TestApp.IStrategy>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
await GeneratorTest<ContextGetGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("TestApp_BattlePanel.ContextGet.g.cs", expected));
|
||||||
|
Assert.Pass();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Generates_Bindings_For_IContextAware_Class()
|
||||||
|
{
|
||||||
|
var source = """
|
||||||
|
using System;
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||||
|
public sealed class GetServiceAttribute : Attribute { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Rule
|
||||||
|
{
|
||||||
|
public interface IContextAware { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Model
|
||||||
|
{
|
||||||
|
public interface IModel { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Systems
|
||||||
|
{
|
||||||
|
public interface ISystem { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Utility
|
||||||
|
{
|
||||||
|
public interface IUtility { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class ContextAwareServiceExtensions
|
||||||
|
{
|
||||||
|
public static T GetService<T>(this object contextAware) => default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public interface IStrategy { }
|
||||||
|
|
||||||
|
public partial class StrategyHost : GFramework.Core.Abstractions.Rule.IContextAware
|
||||||
|
{
|
||||||
|
[GetService]
|
||||||
|
private IStrategy _strategy = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string expected = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using GFramework.Core.Extensions;
|
||||||
|
|
||||||
|
namespace TestApp;
|
||||||
|
|
||||||
|
partial class StrategyHost
|
||||||
|
{
|
||||||
|
private void __InjectContextBindings_Generated()
|
||||||
|
{
|
||||||
|
_strategy = this.GetService<global::TestApp.IStrategy>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
await GeneratorTest<ContextGetGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("TestApp_StrategyHost.ContextGet.g.cs", expected));
|
||||||
|
Assert.Pass();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware()
|
||||||
|
{
|
||||||
|
var source = """
|
||||||
|
using System;
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||||
|
public sealed class GetModelAttribute : Attribute { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Model
|
||||||
|
{
|
||||||
|
public interface IModel { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Systems
|
||||||
|
{
|
||||||
|
public interface ISystem { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Utility
|
||||||
|
{
|
||||||
|
public interface IUtility { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class ContextAwareServiceExtensions
|
||||||
|
{
|
||||||
|
public static T GetModel<T>(this object contextAware) => default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||||
|
|
||||||
|
public partial class InventoryPanel
|
||||||
|
{
|
||||||
|
[GetModel]
|
||||||
|
private IInventoryModel _model = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
TestState =
|
||||||
|
{
|
||||||
|
Sources = { source }
|
||||||
|
},
|
||||||
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
|
};
|
||||||
|
|
||||||
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error)
|
||||||
|
.WithSpan(31, 30, 31, 45)
|
||||||
|
.WithArguments("InventoryPanel"));
|
||||||
|
|
||||||
|
await test.RunAsync();
|
||||||
|
Assert.Pass();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList()
|
||||||
|
{
|
||||||
|
var source = """
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||||
|
public sealed class GetModelsAttribute : Attribute { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Rule
|
||||||
|
{
|
||||||
|
public interface IContextAware { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Model
|
||||||
|
{
|
||||||
|
public interface IModel { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Systems
|
||||||
|
{
|
||||||
|
public interface ISystem { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Abstractions.Utility
|
||||||
|
{
|
||||||
|
public interface IUtility { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Core.Extensions
|
||||||
|
{
|
||||||
|
public static class ContextAwareServiceExtensions
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<T> GetModels<T>(this object contextAware) => default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||||
|
|
||||||
|
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||||
|
{
|
||||||
|
[GetModels]
|
||||||
|
private List<IInventoryModel> _models = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
TestState =
|
||||||
|
{
|
||||||
|
Sources = { source }
|
||||||
|
},
|
||||||
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
|
};
|
||||||
|
|
||||||
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error)
|
||||||
|
.WithSpan(40, 40, 40, 47)
|
||||||
|
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
|
||||||
|
|
||||||
|
await test.RunAsync();
|
||||||
|
Assert.Pass();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,9 +7,15 @@
|
|||||||
-----------------------|----------------------------------|----------|------------------------
|
-----------------------|----------------------------------|----------|------------------------
|
||||||
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
|
||||||
GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
|
||||||
GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
|
||||||
GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer
|
GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
20
GFramework.SourceGenerators/Internals/IsExternalInit.cs
Normal file
20
GFramework.SourceGenerators/Internals/IsExternalInit.cs
Normal 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
|
||||||
46
GFramework.SourceGenerators/README.md
Normal file
46
GFramework.SourceGenerators/README.md
Normal 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` 调用;已显式标记字段的优先级更高。
|
||||||
772
GFramework.SourceGenerators/Rule/ContextGetGenerator.cs
Normal file
772
GFramework.SourceGenerators/Rule/ContextGetGenerator.cs
Normal file
@ -0,0 +1,772 @@
|
|||||||
|
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)
|
||||||
|
];
|
||||||
|
|
||||||
|
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 fieldDeclaration.AttributeLists
|
||||||
|
.SelectMany(static list => list.Attributes)
|
||||||
|
.Any(static attribute => attribute.Name.ToString().Contains("Get", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FieldCandidateInfo? TransformField(GeneratorSyntaxContext context)
|
||||||
|
{
|
||||||
|
if (context.Node is not VariableDeclaratorSyntax variable)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return context.SemanticModel.GetDeclaredSymbol(variable) is IFieldSymbol fieldSymbol
|
||||||
|
? new FieldCandidateInfo(variable, fieldSymbol)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTypeCandidate(SyntaxNode node)
|
||||||
|
{
|
||||||
|
if (node is not ClassDeclarationSyntax classDeclaration)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return classDeclaration.AttributeLists
|
||||||
|
.SelectMany(static list => list.Attributes)
|
||||||
|
.Any(static attribute => attribute.Name.ToString().Contains("GetAll", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypeCandidateInfo? TransformType(GeneratorSyntaxContext context)
|
||||||
|
{
|
||||||
|
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol typeSymbol
|
||||||
|
? 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 = 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));
|
||||||
|
|
||||||
|
var workItems = CollectWorkItems(
|
||||||
|
fieldCandidates,
|
||||||
|
typeCandidates,
|
||||||
|
descriptors,
|
||||||
|
getAllAttribute);
|
||||||
|
|
||||||
|
foreach (var workItem in workItems.Values)
|
||||||
|
{
|
||||||
|
if (!CanGenerateForType(context, workItem, symbols))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var bindings = new List<BindingInfo>();
|
||||||
|
var explicitFields = new HashSet<IFieldSymbol>(SymbolEqualityComparer.Default);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workItem.GetAllDeclaration is not null)
|
||||||
|
{
|
||||||
|
foreach (var field in GetAllFields(workItem.TypeSymbol))
|
||||||
|
{
|
||||||
|
if (explicitFields.Contains(field))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (field.IsStatic)
|
||||||
|
{
|
||||||
|
ReportFieldDiagnostic(
|
||||||
|
context,
|
||||||
|
ContextGetDiagnostics.StaticFieldNotSupported,
|
||||||
|
field);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.IsReadOnly)
|
||||||
|
{
|
||||||
|
ReportFieldDiagnostic(
|
||||||
|
context,
|
||||||
|
ContextGetDiagnostics.ReadOnlyFieldNotSupported,
|
||||||
|
field);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCreateInferredBinding(field, symbols, out var binding))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bindings.Add(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindings.Count == 0 && workItem.GetAllDeclaration is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var source = GenerateSource(workItem.TypeSymbol, bindings);
|
||||||
|
context.AddSource(GetHintName(workItem.TypeSymbol), source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 namedType)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, readOnlyList))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (namedType.TypeArguments.Length != 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
elementType = namedType.TypeArguments[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user