From ab5ea42350ef7277651b90a635dab46c2dd1396d Mon Sep 17 00:00:00 2001 From: GwWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:04:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(logging):=20=E6=B7=BB=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=94=9F=E6=88=90=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 LoggerGenerator 源代码生成器,为标记 LogAttribute 的类自动生成日志字段 - 添加了 LogAttribute 特性,支持配置日志分类、字段名、访问修饰符和静态属性 - 创建了 Diagnostics 静态类,定义 GFLOG001 诊断规则检查 partial 类声明 - 集成 Microsoft.CodeAnalysis 包,启用增量生成器和扩展分析器规则 - 生成的代码包含命名空间、类名和日志字段的完整实现 --- .../GFramework.Generator.Attributes.csproj | 4 + .../generator/logging/LogAttribute.cs | 32 ++++ .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 8 + .../GFramework.Generator.csproj | 1 + .../generator/logging/Diagnostic.cs | 25 +++ .../generator/logging/LoggerGenerator.cs | 149 ++++++++++++++++++ 7 files changed, 222 insertions(+) create mode 100644 GFramework.Generator.Attributes/generator/logging/LogAttribute.cs create mode 100644 GFramework.Generator/AnalyzerReleases.Shipped.md create mode 100644 GFramework.Generator/AnalyzerReleases.Unshipped.md create mode 100644 GFramework.Generator/generator/logging/Diagnostic.cs create mode 100644 GFramework.Generator/generator/logging/LoggerGenerator.cs diff --git a/GFramework.Generator.Attributes/GFramework.Generator.Attributes.csproj b/GFramework.Generator.Attributes/GFramework.Generator.Attributes.csproj index 2b946ab..2bac1cd 100644 --- a/GFramework.Generator.Attributes/GFramework.Generator.Attributes.csproj +++ b/GFramework.Generator.Attributes/GFramework.Generator.Attributes.csproj @@ -3,5 +3,9 @@ netstandard2.0 GeWuYou.GFramework.Generator.Attributes 1.0.0 + 10 + + + diff --git a/GFramework.Generator.Attributes/generator/logging/LogAttribute.cs b/GFramework.Generator.Attributes/generator/logging/LogAttribute.cs new file mode 100644 index 0000000..2214713 --- /dev/null +++ b/GFramework.Generator.Attributes/generator/logging/LogAttribute.cs @@ -0,0 +1,32 @@ +#nullable enable +using System; + +namespace GFramework.Generator.Attributes.generator.logging; + +/// +/// 标注在类上,Source Generator 会为该类自动生成一个日志记录器字段。 +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class LogAttribute : Attribute +{ + /// 日志分类名(默认使用类名) + public string? Category { get; } + + /// 生成字段名 + public string FieldName { get; set; } = "_log"; + + /// 是否生成 static 字段 + public bool IsStatic { get; set; } = true; + + /// 访问修饰符 + public string AccessModifier { get; set; } = "private"; + + /// + /// 初始化 LogAttribute 类的新实例 + /// + /// 日志分类名,默认使用类名 + public LogAttribute(string? category = null) + { + Category = category; + } +} \ No newline at end of file diff --git a/GFramework.Generator/AnalyzerReleases.Shipped.md b/GFramework.Generator/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..60b59dd --- /dev/null +++ b/GFramework.Generator/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/GFramework.Generator/AnalyzerReleases.Unshipped.md b/GFramework.Generator/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..6e2cad7 --- /dev/null +++ b/GFramework.Generator/AnalyzerReleases.Unshipped.md @@ -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 \ No newline at end of file diff --git a/GFramework.Generator/GFramework.Generator.csproj b/GFramework.Generator/GFramework.Generator.csproj index 593796c..f3e46ac 100644 --- a/GFramework.Generator/GFramework.Generator.csproj +++ b/GFramework.Generator/GFramework.Generator.csproj @@ -16,6 +16,7 @@ true Generated + true diff --git a/GFramework.Generator/generator/logging/Diagnostic.cs b/GFramework.Generator/generator/logging/Diagnostic.cs new file mode 100644 index 0000000..6dd2100 --- /dev/null +++ b/GFramework.Generator/generator/logging/Diagnostic.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace GFramework.Generator.generator.logging; + +/// +/// 提供诊断描述符的静态类,用于GFramework日志生成器的编译时检查 +/// +internal static class Diagnostics +{ + /// + /// 定义一个诊断描述符,用于检查使用[Log]特性的类是否声明为partial + /// + /// + /// 当类使用[Log]特性但未声明为partial时,编译器将报告此错误 + /// + 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 + ); +} diff --git a/GFramework.Generator/generator/logging/LoggerGenerator.cs b/GFramework.Generator/generator/logging/LoggerGenerator.cs new file mode 100644 index 0000000..6bed45c --- /dev/null +++ b/GFramework.Generator/generator/logging/LoggerGenerator.cs @@ -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 +{ + /// + /// 日志生成器,用于为标记了LogAttribute的类自动生成日志字段 + /// + [Generator] + public sealed class LoggerGenerator : IIncrementalGenerator + { + private const string AttributeMetadataName = + "GFramework.Generator.Attributes.Logging.LogAttribute"; + + /// + /// 初始化增量生成器 + /// + /// 增量生成器初始化上下文 + 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)); + }); + } + + /// + /// 生成日志字段代码 + /// + /// 类符号 + /// 生成的代码字符串 + 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("// "); + 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(); + } + + /// + /// 获取属性参数的值 + /// + /// 参数类型 + /// 属性数据 + /// 参数名称 + /// 默认值 + /// 参数值或默认值 + private static T GetNamedArg(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; + } + } +}