feat(logging): 添加日志生成器功能

- 实现了 LoggerGenerator 源代码生成器,为标记 LogAttribute 的类自动生成日志字段
- 添加了 LogAttribute 特性,支持配置日志分类、字段名、访问修饰符和静态属性
- 创建了 Diagnostics 静态类,定义 GFLOG001 诊断规则检查 partial 类声明
- 集成 Microsoft.CodeAnalysis 包,启用增量生成器和扩展分析器规则
- 生成的代码包含命名空间、类名和日志字段的完整实现
This commit is contained in:
GwWuYou 2025-12-23 21:04:53 +08:00
parent 5087db9f21
commit ab5ea42350
7 changed files with 222 additions and 0 deletions

View File

@ -3,5 +3,9 @@
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>GeWuYou.GFramework.Generator.Attributes</PackageId>
<Version>1.0.0</Version>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,32 @@
#nullable enable
using System;
namespace GFramework.Generator.Attributes.generator.logging;
/// <summary>
/// 标注在类上Source Generator 会为该类自动生成一个日志记录器字段。
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class LogAttribute : Attribute
{
/// <summary>日志分类名(默认使用类名)</summary>
public string? Category { get; }
/// <summary>生成字段名</summary>
public string FieldName { get; set; } = "_log";
/// <summary>是否生成 static 字段</summary>
public bool IsStatic { get; set; } = true;
/// <summary>访问修饰符</summary>
public string AccessModifier { get; set; } = "private";
/// <summary>
/// 初始化 LogAttribute 类的新实例
/// </summary>
/// <param name="category">日志分类名,默认使用类名</param>
public LogAttribute(string? category = null)
{
Category = category;
}
}

View File

@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

View File

@ -0,0 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
GFLOG001 | GFramework.Logging | Error | Diagnostics

View File

@ -16,6 +16,7 @@
<!-- 有助于调试生成的代码(可选) -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all"/>

View File

@ -0,0 +1,25 @@
using Microsoft.CodeAnalysis;
namespace GFramework.Generator.generator.logging;
/// <summary>
/// 提供诊断描述符的静态类用于GFramework日志生成器的编译时检查
/// </summary>
internal static class Diagnostics
{
/// <summary>
/// 定义一个诊断描述符,用于检查使用[Log]特性的类是否声明为partial
/// </summary>
/// <remarks>
/// 当类使用[Log]特性但未声明为partial时编译器将报告此错误
/// </remarks>
public static readonly DiagnosticDescriptor MustBePartial =
new(
id: "GFLOG001",
title: "Class must be partial",
messageFormat: "Class '{0}' must be declared partial to use [Log]",
category: "GFramework.Logging",
DiagnosticSeverity.Error,
isEnabledByDefault: true
);
}

View File

@ -0,0 +1,149 @@
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace GFramework.Generator.generator.logging
{
/// <summary>
/// 日志生成器用于为标记了LogAttribute的类自动生成日志字段
/// </summary>
[Generator]
public sealed class LoggerGenerator : IIncrementalGenerator
{
private const string AttributeMetadataName =
"GFramework.Generator.Attributes.Logging.LogAttribute";
/// <summary>
/// 初始化增量生成器
/// </summary>
/// <param name="context">增量生成器初始化上下文</param>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. 拿到 LogAttribute Symbol
var logAttributeSymbol =
context.CompilationProvider.Select((compilation, _) =>
compilation.GetTypeByMetadataName(AttributeMetadataName));
// 2. 在 SyntaxProvider 阶段就拿到 SemanticModel
var candidates =
context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax,
static (ctx, _) =>
{
var classDecl = (ClassDeclarationSyntax)ctx.Node;
var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDecl);
return (ClassDecl: classDecl, Symbol: symbol);
})
.Where(x => x.Symbol is not null);
// 3. 合并 Attribute Symbol 并筛选
var targets =
candidates.Combine(logAttributeSymbol)
.Where(pair =>
{
var symbol = pair.Left.Symbol!;
var attrSymbol = pair.Right;
if (attrSymbol is null) return false;
return symbol.GetAttributes().Any(a =>
SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol));
});
// 4. 输出代码
context.RegisterSourceOutput(targets, (spc, pair) =>
{
var classDecl = pair.Left.ClassDecl;
var classSymbol = pair.Left.Symbol!;
// 必须是 partial
if (!classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
spc.ReportDiagnostic(
Diagnostic.Create(
Diagnostics.MustBePartial,
classDecl.Identifier.GetLocation(),
classSymbol.Name
));
return;
}
var source = Generate(classSymbol);
spc.AddSource(
$"{classSymbol.Name}.Logger.g.cs",
SourceText.From(source, Encoding.UTF8));
});
}
/// <summary>
/// 生成日志字段代码
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <returns>生成的代码字符串</returns>
private static string Generate(INamedTypeSymbol classSymbol)
{
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace
? null
: classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
var attr = classSymbol.GetAttributes()
.First(a => a.AttributeClass!.ToDisplayString() == AttributeMetadataName);
var category =
attr.ConstructorArguments.Length > 0 &&
attr.ConstructorArguments[0].Value is string s
? s
: className;
var fieldName = GetNamedArg(attr, "FieldName", "_log");
var access = GetNamedArg(attr, "AccessModifier", "private");
var isStatic = GetNamedArg(attr, "IsStatic", true);
var staticKeyword = isStatic ? "static " : "";
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using GFramework.Core.logging;");
if (ns is not null)
{
sb.AppendLine($"namespace {ns}");
sb.AppendLine("{");
}
sb.AppendLine($" public partial class {className}");
sb.AppendLine(" {");
sb.AppendLine(
$" {access} {staticKeyword}readonly ILog {fieldName} =" +
$" Log.CreateLogger(\"{category}\");");
sb.AppendLine(" }");
if (ns is not null)
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// 获取属性参数的值
/// </summary>
/// <typeparam name="T">参数类型</typeparam>
/// <param name="attr">属性数据</param>
/// <param name="name">参数名称</param>
/// <param name="defaultValue">默认值</param>
/// <returns>参数值或默认值</returns>
private static T GetNamedArg<T>(AttributeData attr, string name, T defaultValue)
{
foreach (var kv in attr.NamedArguments)
{
if (kv.Key == name && kv.Value.Value is T v)
return v;
}
return defaultValue;
}
}
}