diff --git a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs index 493c10e1..a73d076d 100644 --- a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs +++ b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs @@ -1,4 +1,5 @@ -using GFramework.SourceGenerators.Common.Constants; +using GFramework.Core.SourceGenerators.Abstractions.Enums; +using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; using GFramework.SourceGenerators.Common.Generator; @@ -18,6 +19,12 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase /// protected override string AttributeShortNameWithoutSuffix => "GenerateEnumExtensions"; + /// + /// 按元数据名称解析枚举上的 GenerateEnumExtensionsAttribute。 + /// + /// 当前编译上下文。 + /// 待检查的枚举符号。 + /// 匹配到的属性数据;若未标注目标属性则返回 protected override AttributeData? ResolveAttribute(Compilation compilation, INamedTypeSymbol symbol) { var attrSymbol = compilation.GetTypeByMetadataName(AttributeMetadataName); @@ -30,6 +37,15 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)); } + /// + /// 验证候选符号仍然是可生成扩展方法的枚举类型。 + /// + /// 源生成诊断上下文。 + /// 当前编译上下文。 + /// 候选枚举声明语法。 + /// 候选命名类型符号。 + /// 已解析出的生成器属性。 + /// 当符号满足生成前置条件时返回 protected override bool ValidateSymbol(SourceProductionContext context, Compilation compilation, EnumDeclarationSyntax syntax, INamedTypeSymbol symbol, AttributeData attr) @@ -56,6 +72,14 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase ? null : symbol.ContainingNamespace.ToDisplayString(); + var generateIsMethods = GetNamedBooleanArgument( + attr, + nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods), + true); + var generateIsInMethod = GetNamedBooleanArgument( + attr, + nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod), + true); var enumName = symbol.Name; var fullEnumName = symbol.ToDisplayString(); var members = symbol.GetMembers() @@ -74,23 +98,28 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase sb.AppendLine($" public static partial class {enumName}Extensions"); sb.AppendLine(" {"); - // 生成 IsX 方法 - foreach (var memberName in members.Select(m => m.Name)) + // 两个生成开关是彼此独立的契约,需要分别控制输出,并保持空行布局稳定,便于快照精确回归。 + var hasGeneratedMembers = false; + + if (generateIsMethods) { - sb.AppendLine($" /// 是否为 {memberName}"); - sb.AppendLine( - $" public static bool Is{memberName}(this {fullEnumName} value) => value == {fullEnumName}.{memberName};"); - sb.AppendLine(); + hasGeneratedMembers = AppendIsMethods( + sb, + members, + fullEnumName); } - // 生成 IsIn 方法 - sb.AppendLine(" /// 判断是否属于指定集合"); - sb.AppendLine($" public static bool IsIn(this {fullEnumName} value, params {fullEnumName}[] values)"); - sb.AppendLine(" {"); - sb.AppendLine(" if (values == null) return false;"); - sb.AppendLine(" foreach (var v in values) if (value == v) return true;"); - sb.AppendLine(" return false;"); - sb.AppendLine(" }"); + if (generateIsInMethod) + { + if (hasGeneratedMembers) + { + sb.AppendLine(); + } + + AppendIsInMethod( + sb, + fullEnumName); + } sb.AppendLine(" }"); sb.AppendLine("}"); // namespace @@ -107,4 +136,69 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase { return $"{symbol.Name}.EnumExtensions.g.cs"; } + + /// + /// 读取属性上的命名布尔参数,并在参数未显式提供时回退到属性契约默认值。 + /// + /// 待读取的属性数据。 + /// 命名参数名称。 + /// 属性未提供该参数时使用的默认值。 + /// 解析得到的布尔值;若参数缺失或类型不匹配则返回 + private static bool GetNamedBooleanArgument(AttributeData attribute, string argumentName, bool defaultValue) + { + foreach (var namedArgument in attribute.NamedArguments) + { + if (namedArgument.Key == argumentName && + namedArgument.Value.Value is bool value) + { + return value; + } + } + + return defaultValue; + } + + /// + /// 为每个枚举成员追加单值判断扩展方法。 + /// + /// 目标源码构建器。 + /// 需要生成扩展方法的枚举成员。 + /// 枚举的完整类型名。 + /// 当至少生成了一个方法时返回 + private static bool AppendIsMethods(StringBuilder builder, IEnumerable members, string fullEnumName) + { + var hasGeneratedMembers = false; + + foreach (var memberName in members.Select(m => m.Name)) + { + if (hasGeneratedMembers) + { + builder.AppendLine(); + } + + builder.AppendLine($" /// 是否为 {memberName}"); + builder.AppendLine( + $" public static bool Is{memberName}(this {fullEnumName} value) => value == {fullEnumName}.{memberName};"); + hasGeneratedMembers = true; + } + + return hasGeneratedMembers; + } + + /// + /// 追加用于多值匹配的 IsIn 扩展方法。 + /// + /// 目标源码构建器。 + /// 枚举的完整类型名。 + private static void AppendIsInMethod(StringBuilder builder, string fullEnumName) + { + builder.AppendLine(" /// 判断是否属于指定集合"); + builder.AppendLine( + $" public static bool IsIn(this {fullEnumName} value, params {fullEnumName}[] values)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (values == null) return false;"); + builder.AppendLine(" foreach (var v in values) if (value == v) return true;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + } } diff --git a/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs index e5c27226..ce109a36 100644 --- a/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs @@ -4,11 +4,17 @@ using GFramework.SourceGenerators.Tests.Core; namespace GFramework.SourceGenerators.Tests.Enums; +/// +/// 验证枚举扩展生成器在不同属性开关组合下的快照输出。 +/// [TestFixture] public class EnumExtensionsGeneratorSnapshotTests { private const string EnumAttributeNamespace = "GFramework.Core.SourceGenerators.Abstractions.Enums"; + /// + /// 验证默认配置会为普通枚举生成逐项判断方法与集合判断方法。 + /// [Test] public async Task Snapshot_BasicEnum_IsMethods() { @@ -24,14 +30,12 @@ public class EnumExtensionsGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "enums", - "snapshots", - "EnumExtensionsGenerator", - "BasicEnum_IsMethods")); + GetSnapshotFolder("BasicEnum_IsMethods")); } + /// + /// 验证默认配置在较小枚举上仍会生成集合判断方法。 + /// [Test] public async Task Snapshot_BasicEnum_IsInMethod() { @@ -46,14 +50,12 @@ public class EnumExtensionsGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "enums", - "snapshots", - "EnumExtensionsGenerator", - "BasicEnum_IsInMethod")); + GetSnapshotFolder("BasicEnum_IsInMethod")); } + /// + /// 验证带显式位标志值的枚举也会生成对应扩展方法。 + /// [Test] public async Task Snapshot_EnumWithFlagValues() { @@ -71,14 +73,12 @@ public class EnumExtensionsGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "enums", - "snapshots", - "EnumExtensionsGenerator", - "EnumWithFlagValues")); + GetSnapshotFolder("EnumWithFlagValues")); } + /// + /// 验证关闭逐项判断开关后仅保留集合判断方法。 + /// [Test] public async Task Snapshot_DisableIsMethods() { @@ -94,14 +94,12 @@ public class EnumExtensionsGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "enums", - "snapshots", - "EnumExtensionsGenerator", - "DisableIsMethods")); + GetSnapshotFolder("DisableIsMethods")); } + /// + /// 验证关闭集合判断开关后仅保留逐项判断方法。 + /// [Test] public async Task Snapshot_DisableIsInMethod() { @@ -117,16 +115,63 @@ public class EnumExtensionsGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "enums", - "snapshots", - "EnumExtensionsGenerator", - "DisableIsInMethod")); + GetSnapshotFolder("DisableIsInMethod")); } + /// + /// 验证同时关闭两个生成开关时不会输出任何扩展方法。 + /// + [Test] + public async Task Snapshot_DisableAllGeneratedMethods() + { + var source = BuildSource( + """ + public enum Status + { + Active, + Inactive + } + """, + "[GenerateEnumExtensions(GenerateIsMethods = false, GenerateIsInMethod = false)]"); + + await GeneratorSnapshotTest.RunAsync( + source, + GetSnapshotFolder("DisableAllGeneratedMethods")); + } + + /// + /// 将运行时测试目录映射回仓库内已提交的枚举快照目录。 + /// + /// 快照场景名称。 + /// 场景对应的绝对快照目录。 + private static string GetSnapshotFolder(string scenarioName) + { + return Path.GetFullPath( + Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", + "..", + "..", + "Enums", + "snapshots", + "EnumExtensionsGenerator", + scenarioName)); + } + + /// + /// 构造最小自洽的测试输入源码,以稳定驱动枚举扩展生成器的快照测试。 + /// + /// 要注入到测试命名空间中的枚举声明文本。 + /// 枚举上的属性使用方式,默认启用所有生成选项。 + /// 包含内联测试属性与目标枚举声明的完整源码。 + /// + /// 这里内联声明 GenerateEnumExtensionsAttribute,以便每个快照输入保持最小自洽。 + /// 属性命名空间必须与生成器按 metadata name 查找的契约保持一致;如果命名空间、属性名或参数发生变更, + /// 需要同步更新该模板与相关快照,否则测试可能出现静默漂移。 + /// private static string BuildSource(string enumBody, string attributeUsage = "[GenerateEnumExtensions]") { + // 保持属性声明与测试输入同处一个模板中,能够明确锁定生成器对元数据名称和可选参数的语义假设。 return $$""" using System; diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.cs new file mode 100644 index 00000000..2897d09c --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.cs @@ -0,0 +1,21 @@ +// +using System; +namespace TestApp +{ + public static partial class StatusExtensions + { + /// 是否为 Active + public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; + + /// 是否为 Inactive + public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; + + /// 判断是否属于指定集合 + public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) + { + if (values == null) return false; + foreach (var v in values) if (value == v) return true; + return false; + } + } +} diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.cs new file mode 100644 index 00000000..db9fa7ba --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.cs @@ -0,0 +1,24 @@ +// +using System; +namespace TestApp +{ + public static partial class StatusExtensions + { + /// 是否为 Active + public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; + + /// 是否为 Inactive + public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; + + /// 是否为 Pending + public static bool IsPending(this TestApp.Status value) => value == TestApp.Status.Pending; + + /// 判断是否属于指定集合 + public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) + { + if (values == null) return false; + foreach (var v in values) if (value == v) return true; + return false; + } + } +} diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.cs new file mode 100644 index 00000000..74b39b20 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.cs @@ -0,0 +1,8 @@ +// +using System; +namespace TestApp +{ + public static partial class StatusExtensions + { + } +} diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.cs new file mode 100644 index 00000000..3563db50 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.cs @@ -0,0 +1,13 @@ +// +using System; +namespace TestApp +{ + public static partial class StatusExtensions + { + /// 是否为 Active + public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; + + /// 是否为 Inactive + public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; + } +} diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.cs new file mode 100644 index 00000000..41154546 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.cs @@ -0,0 +1,15 @@ +// +using System; +namespace TestApp +{ + public static partial class StatusExtensions + { + /// 判断是否属于指定集合 + public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) + { + if (values == null) return false; + foreach (var v in values) if (value == v) return true; + return false; + } + } +} diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.cs new file mode 100644 index 00000000..ebd87525 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.cs @@ -0,0 +1,27 @@ +// +using System; +namespace TestApp +{ + public static partial class PermissionsExtensions + { + /// 是否为 None + public static bool IsNone(this TestApp.Permissions value) => value == TestApp.Permissions.None; + + /// 是否为 Read + public static bool IsRead(this TestApp.Permissions value) => value == TestApp.Permissions.Read; + + /// 是否为 Write + public static bool IsWrite(this TestApp.Permissions value) => value == TestApp.Permissions.Write; + + /// 是否为 Execute + public static bool IsExecute(this TestApp.Permissions value) => value == TestApp.Permissions.Execute; + + /// 判断是否属于指定集合 + public static bool IsIn(this TestApp.Permissions value, params TestApp.Permissions[] values) + { + if (values == null) return false; + foreach (var v in values) if (value == v) return true; + return false; + } + } +} diff --git a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj index 885865ab..5bd55cf2 100644 --- a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj +++ b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj @@ -29,6 +29,8 @@ + +