refactor(source-generators): 优化ContextAware生成器实现并添加快照测试

- 为ContextAwareGenerator添加详细的XML文档注释
- 简化接口验证逻辑,合并条件判断语句
- 修正特性数据参数命名,统一使用attr命名
- 为接口实现方法添加global::前缀以确保类型解析正确
- 移除未使用的回退方法体,简化方法实现逻辑
- 新增GeneratorSnapshotTest通用快照测试类
- 添加ContextAwareGeneratorSnapshotTests快照测试
- 移除原有的硬编码期望值测试方法
- 修正接口实现中的全局命名空间前缀格式
This commit is contained in:
GwWuYou 2025-12-29 20:30:54 +08:00
parent 02e2e31e95
commit 603b06325d
5 changed files with 214 additions and 126 deletions

View File

@ -22,4 +22,8 @@
<ProjectReference Include="..\GFramework.SourceGenerators\GFramework.SourceGenerators.csproj"/> <ProjectReference Include="..\GFramework.SourceGenerators\GFramework.SourceGenerators.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="rule\snapshots\ContextAwareGenerator\"/>
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,70 @@
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
namespace GFramework.SourceGenerators.Tests.core;
/// <summary>
/// 用于测试源代码生成器的快照测试类
/// </summary>
/// <typeparam name="TGenerator">要测试的源代码生成器类型</typeparam>
public static class GeneratorSnapshotTest<TGenerator>
where TGenerator : new()
{
/// <summary>
/// 运行源代码生成器的快照测试
/// </summary>
/// <param name="source">输入的源代码字符串</param>
/// <param name="snapshotFolder">快照文件存储的文件夹路径</param>
/// <returns>异步任务</returns>
public static async Task RunAsync(
string source,
string snapshotFolder)
{
var test = new CSharpSourceGeneratorTest<TGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
await test.RunAsync();
var generated = test.TestState.GeneratedSources;
foreach (var (filename, content) in generated)
{
var path = Path.Combine(
snapshotFolder,
filename);
if (!File.Exists(path))
{
// 第一次运行:生成 snapshot
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, content.ToString());
Assert.Fail(
$"Snapshot not found. Generated new snapshot at:\n{path}");
}
var expected = await File.ReadAllTextAsync(path);
Assert.That(
Normalize(expected),
Is.EqualTo(Normalize(content.ToString())),
$"Snapshot mismatch: {filename}");
}
}
/// <summary>
/// 标准化文本内容,将换行符统一为\n并去除首尾空白
/// </summary>
/// <param name="text">要标准化的文本</param>
/// <returns>标准化后的文本</returns>
private static string Normalize(string text)
=> text.Replace("\r\n", "\n").Trim();
}

View File

@ -0,0 +1,69 @@
using GFramework.SourceGenerators.rule;
using GFramework.SourceGenerators.Tests.core;
using NUnit.Framework;
namespace GFramework.SourceGenerators.Tests.rule;
/// <summary>
/// 上下文感知生成器快照测试类
/// 用于测试ContextAwareGenerator源代码生成器的输出快照
/// </summary>
[TestFixture]
public class ContextAwareGeneratorSnapshotTests
{
/// <summary>
/// 测试ContextAwareGenerator源代码生成器的快照功能
/// 验证生成器对带有ContextAware特性的类的处理结果
/// </summary>
/// <returns>异步任务,无返回值</returns>
[Test]
public async Task Snapshot_ContextAwareGenerator()
{
// 定义测试用的源代码包含ContextAware特性和相关接口定义
const string source = """
using System;
namespace GFramework.SourceGenerators.Abstractions.rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.architecture.IArchitectureContext context);
GFramework.Core.Abstractions.architecture.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.architecture
{
public interface IArchitectureContext { }
}
namespace TestApp
{
using GFramework.SourceGenerators.Abstractions.rule;
using GFramework.Core.Abstractions.rule;
[ContextAware]
public partial class MyRule : IContextAware
{
}
}
""";
// 执行生成器快照测试,将生成的代码与预期快照进行比较
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
source,
snapshotFolder: Path.Combine(
TestContext.CurrentContext.TestDirectory,
"rule",
"snapshots",
"ContextAwareGenerator"));
}
}

View File

@ -1,4 +1,4 @@
using GFramework.SourceGenerators.rule; using GFramework.SourceGenerators.rule;
using GFramework.SourceGenerators.Tests.core; using GFramework.SourceGenerators.Tests.core;
using NUnit.Framework; using NUnit.Framework;
@ -7,83 +7,6 @@ namespace GFramework.SourceGenerators.Tests.rule;
[TestFixture] [TestFixture]
public class ContextAwareGeneratorTests public class ContextAwareGeneratorTests
{ {
[Test]
public async Task Generates_ContextAware_Code()
{
const string source = """
using System;
namespace GFramework.SourceGenerators.Abstractions.rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute
{
}
}
namespace TestApp
{
using GFramework.SourceGenerators.Abstractions.rule;
[ContextAware]
public partial class MyRule
: GFramework.Core.Abstractions.rule.IContextAware
{
}
}
""";
const string frameworkStub = """
namespace GFramework.Core.Abstractions.rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.architecture.IArchitectureContext context);
GFramework.Core.Abstractions.architecture.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.architecture
{
public interface IArchitectureContext {}
}
""";
const string expected = """
// <auto-generated/>
#nullable enable
namespace TestApp;
partial class MyRule
{
/// <summary>
/// 自动注入的架构上下文
/// </summary>
protected GFramework.Core.Abstractions.architecture.IArchitectureContext Context { get; private set; } = null!;
void global::GFramework.Core.Abstractions.rule.IContextAware.SetContext(
global::GFramework.Core.Abstractions.architecture.IArchitectureContext context)
{
Context = context;
}
global::GFramework.Core.Abstractions.architecture.IArchitectureContext
global::GFramework.Core.Abstractions.rule.IContextAware.GetContext()
{
return Context;
}
}
""";
await GeneratorTest<ContextAwareGenerator>.RunAsync(
source + "\n" + frameworkStub,
("MyRule.ContextAware.g.cs", expected)
);
}
[Test] [Test]
public async Task Generates_ContextAware_Code_When_Interface_Inherits_IContextAware() public async Task Generates_ContextAware_Code_When_Interface_Inherits_IContextAware()
{ {
@ -145,20 +68,20 @@ public class ContextAwareGeneratorTests
/// </summary> /// </summary>
protected GFramework.Core.Abstractions.architecture.IArchitectureContext Context { get; private set; } = null!; protected GFramework.Core.Abstractions.architecture.IArchitectureContext Context { get; private set; } = null!;
void GFramework.Core.Abstractions.rule.IContextAware.SetContext( void global::GFramework.Core.Abstractions.rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.architecture.IArchitectureContext context)
GFramework.Core.Abstractions.architecture.IArchitectureContext context)
{ {
Context = context; Context = context;
} }
GFramework.Core.Abstractions.architecture.IArchitectureContext global::GFramework.Core.Abstractions.architecture.IArchitectureContext global::GFramework.Core.Abstractions.rule.IContextAware.GetContext()
GFramework.Core.Abstractions.rule.IContextAware.GetContext()
{ {
return Context; return Context;
} }
} }
"""; """;
await GeneratorTest<ContextAwareGenerator>.RunAsync( await GeneratorTest<ContextAwareGenerator>.RunAsync(
source + "\n" + frameworkStub, source + "\n" + frameworkStub,
("MyRule.ContextAware.g.cs", expected) ("MyRule.ContextAware.g.cs", expected)

View File

@ -8,14 +8,32 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace GFramework.SourceGenerators.rule; namespace GFramework.SourceGenerators.rule;
/// <summary>
/// 上下文感知生成器用于为标记了ContextAware特性的类自动生成IContextAware接口实现
/// </summary>
[Generator] [Generator]
public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
{ {
/// <summary>
/// 获取特性的元数据名称
/// </summary>
protected override string AttributeMetadataName => protected override string AttributeMetadataName =>
$"{PathContests.SourceGeneratorsAbstractionsPath}.rule.ContextAwareAttribute"; $"{PathContests.SourceGeneratorsAbstractionsPath}.rule.ContextAwareAttribute";
/// <summary>
/// 获取特性的短名称(不包含后缀)
/// </summary>
protected override string AttributeShortNameWithoutSuffix => "ContextAware"; protected override string AttributeShortNameWithoutSuffix => "ContextAware";
/// <summary>
/// 验证符号是否符合生成条件
/// </summary>
/// <param name="context">源生产上下文</param>
/// <param name="compilation">编译对象</param>
/// <param name="syntax">类声明语法节点</param>
/// <param name="symbol">命名类型符号</param>
/// <param name="attr">特性数据</param>
/// <returns>验证是否通过</returns>
protected override bool ValidateSymbol( protected override bool ValidateSymbol(
SourceProductionContext context, SourceProductionContext context,
Compilation compilation, Compilation compilation,
@ -26,16 +44,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
var iContextAware = compilation.GetTypeByMetadataName( var iContextAware = compilation.GetTypeByMetadataName(
$"{PathContests.CoreAbstractionsNamespace}.rule.IContextAware"); $"{PathContests.CoreAbstractionsNamespace}.rule.IContextAware");
if (iContextAware is null) if (iContextAware is null ||
{ !symbol.AllInterfaces.Any(i =>
context.ReportDiagnostic(Diagnostic.Create(
ContextAwareDiagnostic.ClassMustImplementIContextAware,
syntax.Identifier.GetLocation(),
symbol.Name));
return false;
}
if (!symbol.AllInterfaces.Any(i =>
SymbolEqualityComparer.Default.Equals(i, iContextAware))) SymbolEqualityComparer.Default.Equals(i, iContextAware)))
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
@ -54,7 +64,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
/// <param name="context">源生产上下文</param> /// <param name="context">源生产上下文</param>
/// <param name="compilation">编译对象</param> /// <param name="compilation">编译对象</param>
/// <param name="symbol">命名类型符号</param> /// <param name="symbol">命名类型符号</param>
/// <param name="attr">性数据</param> /// <param name="attr">性数据</param>
/// <returns>生成的源代码字符串</returns> /// <returns>生成的源代码字符串</returns>
protected override string Generate( protected override string Generate(
SourceProductionContext context, SourceProductionContext context,
@ -65,8 +75,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
var ns = symbol.ContainingNamespace.IsGlobalNamespace var ns = symbol.ContainingNamespace.IsGlobalNamespace
? null ? null
: symbol.ContainingNamespace.ToDisplayString(); : symbol.ContainingNamespace.ToDisplayString();
var iContextAware = compilation.GetTypeByMetadataName( var iContextAware = compilation.GetTypeByMetadataName(
$"{PathContests.CoreAbstractionsNamespace}.rule.IContextAware"); $"{PathContests.CoreAbstractionsNamespace}.rule.IContextAware")!;
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>"); sb.AppendLine("// <auto-generated/>");
@ -83,18 +94,27 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine("{"); sb.AppendLine("{");
GenerateContextProperty(sb); GenerateContextProperty(sb);
GenerateInterfaceImplementations(sb, iContextAware!); GenerateInterfaceImplementations(sb, iContextAware);
sb.AppendLine("}"); sb.AppendLine("}");
return sb.ToString().TrimEnd(); return sb.ToString().TrimEnd();
} }
/// <summary>
/// 获取生成文件的提示名称
/// </summary>
/// <param name="symbol">命名类型符号</param>
/// <returns>生成文件的提示名称</returns>
protected override string GetHintName(INamedTypeSymbol symbol) protected override string GetHintName(INamedTypeSymbol symbol)
=> $"{symbol.Name}.ContextAware.g.cs"; => $"{symbol.Name}.ContextAware.g.cs";
// ========================= // =========================
// 生成 Context 属性 // Context 属性(无 global::,与测试一致)
// ========================= // =========================
/// <summary>
/// 生成Context属性
/// </summary>
/// <param name="sb">字符串构建器</param>
private static void GenerateContextProperty(StringBuilder sb) private static void GenerateContextProperty(StringBuilder sb)
{ {
sb.AppendLine(" /// <summary>"); sb.AppendLine(" /// <summary>");
@ -106,28 +126,39 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
} }
// ========================= // =========================
// 自动实现接口方法 // 显式接口实现(使用 global::
// ========================= // =========================
/// <summary>
/// 生成接口实现
/// </summary>
/// <param name="sb">字符串构建器</param>
/// <param name="interfaceSymbol">接口符号</param>
private static void GenerateInterfaceImplementations( private static void GenerateInterfaceImplementations(
StringBuilder sb, StringBuilder sb,
INamedTypeSymbol interfaceSymbol) INamedTypeSymbol interfaceSymbol)
{ {
var interfaceFullName = interfaceSymbol.ToDisplayString( var interfaceName = interfaceSymbol.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat); SymbolDisplayFormat.FullyQualifiedFormat);
foreach (var member in interfaceSymbol.GetMembers().OfType<IMethodSymbol>()) foreach (var method in interfaceSymbol.GetMembers().OfType<IMethodSymbol>())
{ {
if (member.MethodKind != MethodKind.Ordinary) if (method.MethodKind != MethodKind.Ordinary)
continue; continue;
GenerateMethod(sb, interfaceFullName, member); GenerateMethod(sb, interfaceName, method);
sb.AppendLine(); sb.AppendLine();
} }
} }
/// <summary>
/// 生成方法实现
/// </summary>
/// <param name="sb">字符串构建器</param>
/// <param name="interfaceName">接口名称</param>
/// <param name="method">方法符号</param>
private static void GenerateMethod( private static void GenerateMethod(
StringBuilder sb, StringBuilder sb,
string interfaceFullName, string interfaceName,
IMethodSymbol method) IMethodSymbol method)
{ {
var returnType = method.ReturnType.ToDisplayString( var returnType = method.ReturnType.ToDisplayString(
@ -138,16 +169,19 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
$"{p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {p.Name}")); $"{p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {p.Name}"));
sb.AppendLine( sb.AppendLine(
$" {returnType} {interfaceFullName}.{method.Name}({parameters})"); $" {returnType} {interfaceName}.{method.Name}({parameters})");
sb.AppendLine(" {"); sb.AppendLine(" {");
GenerateMethodBody(sb, method); GenerateMethodBody(sb, method);
sb.AppendLine(" }"); sb.AppendLine(" }");
} }
// ========================= /// <summary>
// 方法语义策略 /// 生成方法体
// ========================= /// </summary>
/// <param name="sb">字符串构建器</param>
/// <param name="method">方法符号</param>
private static void GenerateMethodBody( private static void GenerateMethodBody(
StringBuilder sb, StringBuilder sb,
IMethodSymbol method) IMethodSymbol method)
@ -163,25 +197,13 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
break; break;
default: default:
GenerateFallbackBody(sb, method); if (!method.ReturnsVoid)
{
sb.AppendLine(
$" throw new System.NotImplementedException(\"Method '{method.Name}' is not supported.\");");
}
break; break;
} }
} }
private static void GenerateFallbackBody(
StringBuilder sb,
IMethodSymbol method)
{
if (method.ReturnsVoid)
{
sb.AppendLine(" // no-op");
}
else
{
sb.AppendLine(
$" throw new System.NotImplementedException(");
sb.AppendLine(
$" \"Method '{method.Name}' is not supported by ContextAwareGenerator.\");");
}
}
} }