mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-28 16:13:28 +08:00
refactor(generators): 优化ContextGetGenerator代码结构并改进异常处理
- 修改GetService方法文档,将返回值描述从"返回null"改为"抛出异常" - 为GetService方法添加InvalidOperationException异常说明 - 删除冗余的IsExternalInit类文件 - 重构属性匹配逻辑,使用预定义集合进行候选属性名称验证 - 添加辅助方法HasCandidateAttribute、TryGetAttributeSimpleName等提升代码可读性 - 改进集合类型推断逻辑,支持接口类型的遍历匹配 - 更新单元测试以验证完全限定名属性和只读列表类型的支持 - 修正诊断错误位置信息的准确性
This commit is contained in:
parent
ee2936e0a2
commit
796408539e
@ -18,8 +18,9 @@ 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>
|
||||||
|
/// <exception cref="InvalidOperationException">当指定服务未注册时抛出</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);
|
||||||
|
|||||||
@ -101,6 +101,87 @@ public class ContextGetGeneratorTests
|
|||||||
Assert.Pass();
|
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]
|
[Test]
|
||||||
public async Task Generates_Inferred_Bindings_For_GetAll_Class()
|
public async Task Generates_Inferred_Bindings_For_GetAll_Class()
|
||||||
{
|
{
|
||||||
@ -345,7 +426,7 @@ public class ContextGetGeneratorTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error)
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error)
|
||||||
.WithSpan(31, 30, 31, 45)
|
.WithSpan(40, 33, 40, 39)
|
||||||
.WithArguments("InventoryPanel"));
|
.WithArguments("InventoryPanel"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
@ -416,10 +497,88 @@ public class ContextGetGeneratorTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error)
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error)
|
||||||
.WithSpan(40, 40, 40, 47)
|
.WithSpan(46, 39, 46, 46)
|
||||||
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
|
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
Assert.Pass();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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 本体 -->
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -89,6 +89,20 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
true)
|
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)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
{
|
{
|
||||||
var fieldCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
var fieldCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||||
@ -125,9 +139,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
})
|
})
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return fieldDeclaration.AttributeLists
|
return HasCandidateAttribute(fieldDeclaration.AttributeLists, FieldCandidateAttributeNames);
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute => attribute.Name.ToString().Contains("Get", StringComparison.Ordinal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FieldCandidateInfo? TransformField(GeneratorSyntaxContext context)
|
private static FieldCandidateInfo? TransformField(GeneratorSyntaxContext context)
|
||||||
@ -135,7 +147,10 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
if (context.Node is not VariableDeclaratorSyntax variable)
|
if (context.Node is not VariableDeclaratorSyntax variable)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return context.SemanticModel.GetDeclaredSymbol(variable) is IFieldSymbol fieldSymbol
|
if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return HasAnyBindingAttribute(fieldSymbol, context.SemanticModel.Compilation)
|
||||||
? new FieldCandidateInfo(variable, fieldSymbol)
|
? new FieldCandidateInfo(variable, fieldSymbol)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@ -145,9 +160,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
if (node is not ClassDeclarationSyntax classDeclaration)
|
if (node is not ClassDeclarationSyntax classDeclaration)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return classDeclaration.AttributeLists
|
return HasCandidateAttribute(classDeclaration.AttributeLists, TypeCandidateAttributeNames);
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute => attribute.Name.ToString().Contains("GetAll", StringComparison.Ordinal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TypeCandidateInfo? TransformType(GeneratorSyntaxContext context)
|
private static TypeCandidateInfo? TransformType(GeneratorSyntaxContext context)
|
||||||
@ -155,7 +168,10 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol typeSymbol
|
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return HasAttribute(typeSymbol, context.SemanticModel.Compilation, GetAllAttributeMetadataName)
|
||||||
? new TypeCandidateInfo(classDeclaration, typeSymbol)
|
? new TypeCandidateInfo(classDeclaration, typeSymbol)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@ -317,6 +333,54 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
return false;
|
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(
|
private static Dictionary<INamedTypeSymbol, TypeWorkItem> CollectWorkItems(
|
||||||
ImmutableArray<FieldCandidateInfo?> fieldCandidates,
|
ImmutableArray<FieldCandidateInfo?> fieldCandidates,
|
||||||
ImmutableArray<TypeCandidateInfo?> typeCandidates,
|
ImmutableArray<TypeCandidateInfo?> typeCandidates,
|
||||||
@ -654,19 +718,34 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
{
|
{
|
||||||
elementType = null!;
|
elementType = null!;
|
||||||
|
|
||||||
if (readOnlyList is null || fieldType is not INamedTypeSymbol namedType)
|
if (readOnlyList is null || fieldType is not INamedTypeSymbol targetType)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, readOnlyList))
|
foreach (var candidateType in EnumerateCollectionTypeCandidates(targetType))
|
||||||
return false;
|
{
|
||||||
|
if (candidateType.TypeArguments.Length != 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (namedType.TypeArguments.Length != 1)
|
var candidateElementType = candidateType.TypeArguments[0];
|
||||||
return false;
|
var expectedSourceType = readOnlyList.Construct(candidateElementType);
|
||||||
|
if (!expectedSourceType.IsAssignableTo(targetType))
|
||||||
|
continue;
|
||||||
|
|
||||||
elementType = namedType.TypeArguments[0];
|
elementType = candidateElementType;
|
||||||
return true;
|
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)
|
private static IEnumerable<IFieldSymbol> GetAllFields(INamedTypeSymbol typeSymbol)
|
||||||
{
|
{
|
||||||
return typeSymbol.GetMembers()
|
return typeSymbol.GetMembers()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user