From 603b06325defe3c4fdc2132640fb91613cdefeab Mon Sep 17 00:00:00 2001 From: GwWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 29 Dec 2025 20:30:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(source-generators):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96ContextAware=E7=94=9F=E6=88=90=E5=99=A8=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=BF=AB=E7=85=A7=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为ContextAwareGenerator添加详细的XML文档注释 - 简化接口验证逻辑,合并条件判断语句 - 修正特性数据参数命名,统一使用attr命名 - 为接口实现方法添加global::前缀以确保类型解析正确 - 移除未使用的回退方法体,简化方法实现逻辑 - 新增GeneratorSnapshotTest通用快照测试类 - 添加ContextAwareGeneratorSnapshotTests快照测试 - 移除原有的硬编码期望值测试方法 - 修正接口实现中的全局命名空间前缀格式 --- .../GFramework.SourceGenerators.Tests.csproj | 4 + .../core/GeneratorSnapshotTest.cs | 70 +++++++++++ .../ContextAwareGeneratorSnapshotTests.cs | 69 +++++++++++ .../rule/ContextAwareGeneratorTests.cs | 87 +------------- .../rule/ContextAwareGenerator.cs | 110 +++++++++++------- 5 files changed, 214 insertions(+), 126 deletions(-) create mode 100644 GFramework.SourceGenerators.Tests/core/GeneratorSnapshotTest.cs create mode 100644 GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorSnapshotTests.cs diff --git a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj index 75655b0..073a052 100644 --- a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj +++ b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj @@ -22,4 +22,8 @@ + + + + diff --git a/GFramework.SourceGenerators.Tests/core/GeneratorSnapshotTest.cs b/GFramework.SourceGenerators.Tests/core/GeneratorSnapshotTest.cs new file mode 100644 index 0000000..23e0e6b --- /dev/null +++ b/GFramework.SourceGenerators.Tests/core/GeneratorSnapshotTest.cs @@ -0,0 +1,70 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace GFramework.SourceGenerators.Tests.core; + +/// +/// 用于测试源代码生成器的快照测试类 +/// +/// 要测试的源代码生成器类型 +public static class GeneratorSnapshotTest + where TGenerator : new() +{ + /// + /// 运行源代码生成器的快照测试 + /// + /// 输入的源代码字符串 + /// 快照文件存储的文件夹路径 + /// 异步任务 + public static async Task RunAsync( + string source, + string snapshotFolder) + { + var test = new CSharpSourceGeneratorTest + { + 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}"); + } + } + + /// + /// 标准化文本内容,将换行符统一为\n并去除首尾空白 + /// + /// 要标准化的文本 + /// 标准化后的文本 + private static string Normalize(string text) + => text.Replace("\r\n", "\n").Trim(); +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorSnapshotTests.cs new file mode 100644 index 0000000..f4fd820 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorSnapshotTests.cs @@ -0,0 +1,69 @@ +using GFramework.SourceGenerators.rule; +using GFramework.SourceGenerators.Tests.core; +using NUnit.Framework; + +namespace GFramework.SourceGenerators.Tests.rule; + +/// +/// 上下文感知生成器快照测试类 +/// 用于测试ContextAwareGenerator源代码生成器的输出快照 +/// +[TestFixture] +public class ContextAwareGeneratorSnapshotTests +{ + /// + /// 测试ContextAwareGenerator源代码生成器的快照功能 + /// 验证生成器对带有ContextAware特性的类的处理结果 + /// + /// 异步任务,无返回值 + [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.RunAsync( + source, + snapshotFolder: Path.Combine( + TestContext.CurrentContext.TestDirectory, + "rule", + "snapshots", + "ContextAwareGenerator")); + } +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorTests.cs b/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorTests.cs index 3c36dd7..a714823 100644 --- a/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/rule/ContextAwareGeneratorTests.cs @@ -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 = """ - // - #nullable enable - - namespace TestApp; - - partial class MyRule - { - /// - /// 自动注入的架构上下文 - /// - 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.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 /// 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.RunAsync( source + "\n" + frameworkStub, ("MyRule.ContextAware.g.cs", expected) diff --git a/GFramework.SourceGenerators/rule/ContextAwareGenerator.cs b/GFramework.SourceGenerators/rule/ContextAwareGenerator.cs index 5d825b4..199b7f4 100644 --- a/GFramework.SourceGenerators/rule/ContextAwareGenerator.cs +++ b/GFramework.SourceGenerators/rule/ContextAwareGenerator.cs @@ -8,14 +8,32 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; namespace GFramework.SourceGenerators.rule; +/// +/// 上下文感知生成器,用于为标记了ContextAware特性的类自动生成IContextAware接口实现 +/// [Generator] public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase { + /// + /// 获取特性的元数据名称 + /// protected override string AttributeMetadataName => $"{PathContests.SourceGeneratorsAbstractionsPath}.rule.ContextAwareAttribute"; + /// + /// 获取特性的短名称(不包含后缀) + /// protected override string AttributeShortNameWithoutSuffix => "ContextAware"; + /// + /// 验证符号是否符合生成条件 + /// + /// 源生产上下文 + /// 编译对象 + /// 类声明语法节点 + /// 命名类型符号 + /// 特性数据 + /// 验证是否通过 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 } /// - /// 生成源代码 + /// 生成源代码 /// /// 源生产上下文 /// 编译对象 /// 命名类型符号 - /// 属性数据 + /// 特性数据 /// 生成的源代码字符串 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("// "); @@ -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(); } + /// + /// 获取生成文件的提示名称 + /// + /// 命名类型符号 + /// 生成文件的提示名称 protected override string GetHintName(INamedTypeSymbol symbol) => $"{symbol.Name}.ContextAware.g.cs"; // ========================= - // 生成 Context 属性 + // Context 属性(无 global::,与测试一致) // ========================= + /// + /// 生成Context属性 + /// + /// 字符串构建器 private static void GenerateContextProperty(StringBuilder sb) { sb.AppendLine(" /// "); @@ -106,28 +126,39 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase } // ========================= - // 自动实现接口方法 + // 显式接口实现(使用 global::) // ========================= + /// + /// 生成接口实现 + /// + /// 字符串构建器 + /// 接口符号 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()) + foreach (var method in interfaceSymbol.GetMembers().OfType()) { - if (member.MethodKind != MethodKind.Ordinary) + if (method.MethodKind != MethodKind.Ordinary) continue; - GenerateMethod(sb, interfaceFullName, member); + GenerateMethod(sb, interfaceName, method); sb.AppendLine(); } } + /// + /// 生成方法实现 + /// + /// 字符串构建器 + /// 接口名称 + /// 方法符号 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(" }"); } - // ========================= - // 方法语义策略 - // ========================= + /// + /// 生成方法体 + /// + /// 字符串构建器 + /// 方法符号 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.\");"); - } - } } \ No newline at end of file