feat(generator): 添加枚举扩展方法生成器及相关属性

- 新增 EnumExtensionsGenerator 源生成器
- 实现 GenerateEnumExtensionsAttribute 特性标注
- 为标记的枚举自动生成 IsXXX 和 IsIn 扩展方法
- 配置项目引用及 Analyzer 打包设置
- 更新解决方案文件包含新增项目
- 调整主项目配置排除生成器相关文件编译
This commit is contained in:
GwWuYou 2025-12-10 08:35:11 +08:00
parent 569713b41d
commit 50a71deaa7
7 changed files with 240 additions and 11 deletions

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>GeWuYou.GFramework.Generator.Attributes</PackageId>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,22 @@

using System;
namespace GFramework.Generator.Attributes
{
/// <summary>
/// 标注在 enum 上Source Generator 会为该 enum 生成扩展方法。
/// </summary>
[AttributeUsage(AttributeTargets.Enum)]
public sealed class GenerateEnumExtensionsAttribute : Attribute
{
/// <summary>
/// 是否为每个枚举项生成单独的 IsXXX 方法(默认 true
/// </summary>
public bool GenerateIsMethods { get; set; } = true;
/// <summary>
/// 是否生成一个 IsIn(params T[]) 方法以简化多值判断(默认 true
/// </summary>
public bool GenerateIsInMethod { get; set; } = true;
}
}

View File

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>GeWuYou.GFramework.Generator</PackageId>
<LangVersion>latest</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<!-- 对 generator 项目要启用扩展规则 -->
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<!-- 不把输出当作运行时库 -->
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- 有助于调试生成的代码(可选) -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<!-- Generator 需要引用 Attributes 项目,但不作为运行时依赖 -->
<ProjectReference Include="..\GFramework.Generator.Attributes\GFramework.Generator.Attributes.csproj"
PrivateAssets="all"/>
</ItemGroup>
<!-- 将 Generator 和 Attributes DLL 打包为 Analyzer -->
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false"/>
<None Include="$(OutputPath)\GFramework.Generator.Attributes.dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,125 @@
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace GFramework.Generator.generator.enums
{
[Generator]
public class EnumExtensionsGenerator : IIncrementalGenerator
{
private const string AttributeFullName = "GFramework.Generator.Attributes.GenerateEnumExtensionsAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. 找到所有 EnumDeclarationSyntax 节点
var enumDecls = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is EnumDeclarationSyntax,
transform: (ctx, _) =>
(EnumDecl: (EnumDeclarationSyntax)ctx.Node, ctx.SemanticModel))
.Where(t => t.EnumDecl != null);
// 2. 解析为 symbol 并过滤带 Attribute 的 enum
var enumSymbols = enumDecls
.Select((t, _) =>
{
var model = t.SemanticModel;
var enumDecl = t.EnumDecl;
var symbol = model.GetDeclaredSymbol(enumDecl) as INamedTypeSymbol;
return symbol;
})
.Where(symbol => symbol != null)
.Select((symbol, _) =>
{
// 判断是否包含我们的 Attribute
var hasAttr = symbol.GetAttributes().Any(ad =>
ad.AttributeClass?.ToDisplayString() == AttributeFullName ||
ad.AttributeClass?.ToDisplayString().EndsWith(".GenerateEnumExtensionsAttribute") == true);
return (Symbol: symbol, HasAttr: hasAttr);
})
.Where(x => x.HasAttr)
.Collect();
// 3. 为每个 enum 生成代码
context.RegisterSourceOutput(enumSymbols, (spc, list) =>
{
foreach (var enumSymbol in list.Select(item => item.Symbol))
{
try
{
var src = GenerateForEnum(enumSymbol);
var hintName = $"{enumSymbol.Name}.EnumExtensions.g.cs";
spc.AddSource(hintName, SourceText.From(src, Encoding.UTF8));
}
catch (Exception ex)
{
// 发生异常时生成一个注释文件(避免完全静默失败)
var err = $"// EnumExtensionsGenerator failed for {enumSymbol?.Name}: {ex.Message}";
spc.AddSource($"{enumSymbol?.Name}.EnumExtensions.Error.g.cs",
SourceText.From(err, Encoding.UTF8));
}
}
});
}
private static string GenerateForEnum(INamedTypeSymbol enumSymbol)
{
var ns = enumSymbol.ContainingNamespace.IsGlobalNamespace
? null
: enumSymbol.ContainingNamespace.ToDisplayString();
var enumName = enumSymbol.Name;
var fullEnumName = enumSymbol.ToDisplayString(); // 包含命名空间
var members = enumSymbol.GetMembers().OfType<IFieldSymbol>().Where(f => f.ConstantValue != null).ToArray();
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using System;");
if (!string.IsNullOrEmpty(ns))
{
sb.AppendLine($"namespace {ns}");
sb.AppendLine("{");
}
else
{
sb.AppendLine("namespace EnumExtensionsGenerated");
sb.AppendLine("{");
}
sb.AppendLine($" public static partial class {enumName}Extensions");
sb.AppendLine(" {");
// 1. 单项 IsX 方法
// 替换原第93行开始的 foreach 块
var memberChecks = members.Select(m =>
{
var memberName = m.Name;
var safeMethodName = $"Is{memberName}";
return $@" /// <summary>Auto-generated: 是否为 {memberName}</summary>
public static bool {safeMethodName}(this {fullEnumName} value) => value == {fullEnumName}.{memberName};
";
}).ToArray();
sb.Append(string.Join("", memberChecks));
// 2. IsIn(params ...) 方法
sb.AppendLine($" /// <summary>Auto-generated: 判断是否属于指定集合</summary>");
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(" }");
sb.AppendLine(" }");
sb.AppendLine("}"); // namespace
return sb.ToString();
}
}
}

View File

@ -22,7 +22,29 @@
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks> <TargetFrameworks>net9.0;net8.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" /> <None Include="README.md" Pack="true" PackagePath="" />
<None Remove="GFramework.Generator\**" />
<None Remove="GFramework.Generator.Attributes\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove="GFramework.Generator\**" />
<Compile Remove="GFramework.Generator.Attributes\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="GFramework.Generator\**" />
<EmbeddedResource Remove="GFramework.Generator.Attributes\**" />
</ItemGroup>
<!-- 引用 Source Generator -->
<ItemGroup>
<ProjectReference Include="GFramework.Generator\GFramework.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"/>
<ProjectReference Include="GFramework.Generator.Attributes\GFramework.Generator.Attributes.csproj"/>
</ItemGroup>
</Project> </Project>

View File

@ -2,6 +2,10 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework ", "GFramework.csproj", "{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework ", "GFramework.csproj", "{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Generator", "GFramework.Generator\GFramework.Generator.csproj", "{E9D51809-0351-4B83-B85B-B5F469AAB3B8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Generator.Attributes", "GFramework.Generator.Attributes\GFramework.Generator.Attributes.csproj", "{84C5C3C9-5620-4924-BA04-92F813F2B70F}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -12,5 +16,13 @@ Global
{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Release|Any CPU.Build.0 = Release|Any CPU {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Release|Any CPU.Build.0 = Release|Any CPU
{E9D51809-0351-4B83-B85B-B5F469AAB3B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9D51809-0351-4B83-B85B-B5F469AAB3B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9D51809-0351-4B83-B85B-B5F469AAB3B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9D51809-0351-4B83-B85B-B5F469AAB3B8}.Release|Any CPU.Build.0 = Release|Any CPU
{84C5C3C9-5620-4924-BA04-92F813F2B70F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84C5C3C9-5620-4924-BA04-92F813F2B70F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84C5C3C9-5620-4924-BA04-92F813F2B70F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84C5C3C9-5620-4924-BA04-92F813F2B70F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,4 +1,4 @@
using GFramework.framework.events; using GFramework.framework.events;
namespace GFramework.framework.property; namespace GFramework.framework.property;
@ -8,14 +8,14 @@ namespace GFramework.framework.property;
/// </summary> /// </summary>
/// <typeparam name="T">属性值的类型</typeparam> /// <typeparam name="T">属性值的类型</typeparam>
/// <param name="defaultValue">属性的默认值</param> /// <param name="defaultValue">属性的默认值</param>
public class BindableProperty<T>(T defaultValue = default) : IBindableProperty<T> public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<T>
{ {
protected T MValue = defaultValue; protected T MValue = defaultValue;
/// <summary> /// <summary>
/// 获取或设置属性值比较器默认使用Equals方法进行比较 /// 获取或设置属性值比较器默认使用Equals方法进行比较
/// </summary> /// </summary>
public static Func<T, T, bool> Comparer { get; set; } = (a, b) => a.Equals(b); public static Func<T, T, bool> Comparer { get; set; } = (a, b) => a!.Equals(b)!;
/// <summary> /// <summary>
/// 设置自定义比较器 /// 设置自定义比较器
@ -37,12 +37,12 @@ public class BindableProperty<T>(T defaultValue = default) : IBindableProperty<T
set set
{ {
// 使用 default(T) 替代 null 比较,避免 SonarQube 警告 // 使用 default(T) 替代 null 比较,避免 SonarQube 警告
if (EqualityComparer<T>.Default.Equals(value, default) && if (EqualityComparer<T>.Default.Equals(value, default!) &&
EqualityComparer<T>.Default.Equals(MValue, default)) EqualityComparer<T>.Default.Equals(MValue, default!))
return; return;
// 若新值与旧值相等则不执行后续操作 // 若新值与旧值相等则不执行后续操作
if (!EqualityComparer<T>.Default.Equals(value, default) && Comparer(value, MValue)) if (!EqualityComparer<T>.Default.Equals(value, default!) && Comparer(value, MValue))
return; return;
SetValue(value); SetValue(value);
@ -68,7 +68,7 @@ public class BindableProperty<T>(T defaultValue = default) : IBindableProperty<T
/// <param name="newValue">新的属性值</param> /// <param name="newValue">新的属性值</param>
public void SetValueWithoutEvent(T newValue) => MValue = newValue; public void SetValueWithoutEvent(T newValue) => MValue = newValue;
private Action<T> _mOnValueChanged = (_) => { }; private Action<T>? _mOnValueChanged = null;
/// <summary> /// <summary>
/// 注册属性值变化事件回调 /// 注册属性值变化事件回调
@ -106,13 +106,15 @@ public class BindableProperty<T>(T defaultValue = default) : IBindableProperty<T
IUnRegister IEasyEvent.Register(Action onEvent) IUnRegister IEasyEvent.Register(Action onEvent)
{ {
return Register(Action); return Register(Action);
void Action(T _) => onEvent(); void Action(T _)
{
onEvent();
}
} }
/// <summary> /// <summary>
/// 返回属性值的字符串表示形式 /// 返回属性值的字符串表示形式
/// </summary> /// </summary>
/// <returns>属性值的字符串表示</returns> /// <returns>属性值的字符串表示</returns>
public override string ToString() => Value.ToString(); public override string ToString() => Value?.ToString() ?? string.Empty;
} }