mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
refactor(source-generators): 优化ContextAware生成器实现并添加快照测试
- 为ContextAwareGenerator添加详细的XML文档注释 - 简化接口验证逻辑,合并条件判断语句 - 修正特性数据参数命名,统一使用attr命名 - 为接口实现方法添加global::前缀以确保类型解析正确 - 移除未使用的回退方法体,简化方法实现逻辑 - 新增GeneratorSnapshotTest通用快照测试类 - 添加ContextAwareGeneratorSnapshotTests快照测试 - 移除原有的硬编码期望值测试方法 - 修正接口实现中的全局命名空间前缀格式
This commit is contained in:
parent
02e2e31e95
commit
603b06325d
@ -22,4 +22,8 @@
|
||||
<ProjectReference Include="..\GFramework.SourceGenerators\GFramework.SourceGenerators.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="rule\snapshots\ContextAwareGenerator\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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.\");");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user