GwWuYou ab5ea42350 feat(logging): 添加日志生成器功能
- 实现了 LoggerGenerator 源代码生成器,为标记 LogAttribute 的类自动生成日志字段
- 添加了 LogAttribute 特性,支持配置日志分类、字段名、访问修饰符和静态属性
- 创建了 Diagnostics 静态类,定义 GFLOG001 诊断规则检查 partial 类声明
- 集成 Microsoft.CodeAnalysis 包,启用增量生成器和扩展分析器规则
- 生成的代码包含命名空间、类名和日志字段的完整实现
2025-12-23 21:04:53 +08:00

150 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}