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"/>
</ItemGroup>
<ItemGroup>
<Folder Include="rule\snapshots\ContextAwareGenerator\"/>
</ItemGroup>
</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 NUnit.Framework;
@ -7,83 +7,6 @@ namespace GFramework.SourceGenerators.Tests.rule;
[TestFixture]
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]
public async Task Generates_ContextAware_Code_When_Interface_Inherits_IContextAware()
{
@ -145,20 +68,20 @@ public class ContextAwareGeneratorTests
/// </summary>
protected GFramework.Core.Abstractions.architecture.IArchitectureContext Context { get; private set; } = null!;
void GFramework.Core.Abstractions.rule.IContextAware.SetContext(
GFramework.Core.Abstractions.architecture.IArchitectureContext context)
void global::GFramework.Core.Abstractions.rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.architecture.IArchitectureContext context)
{
Context = context;
}
GFramework.Core.Abstractions.architecture.IArchitectureContext
GFramework.Core.Abstractions.rule.IContextAware.GetContext()
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)

View File

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