refactor(GFramework.Generator): 重构代码生成器结构并添加Godot日志生成器

- 移除原有的LoggerGenerator.cs和相关README文档
- 重命名GFramework.Generator为GFramework.SourceGenerators
- 重命名GFramework.Generator.Attributes为GFramework.SourceGenerators.Attributes
- 添加新的Godot日志生成器(GodotLoggerGenerator)及对应属性(GodotLogAttribute)
- 创建GFramework.Godot.SourceGenerators新项目用于Godot特定功能
- 修改日志生成器使用GodotLoggerFactory而非ConsoleLoggerFactory
This commit is contained in:
GwWuYou 2025-12-25 21:10:17 +08:00
parent 7fa2a1e4cb
commit 763b460575
21 changed files with 568 additions and 669 deletions

View File

@ -1,4 +0,0 @@
项目额外的简单代码生成器
目前已有的功能
- 为枚举添加两个扩展方法方便判断枚举值

View File

@ -1,185 +0,0 @@
#nullable enable
using System;
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
{
// 请确保这里的命名空间和类名与 Attributes 项目中定义的完全一致(注意大小写!)
private const string AttributeMetadataName = "GFramework.Generator.Attributes.generator.logging.LogAttribute";
private const string AttributeShortName = "LogAttribute";
private const string AttributeShortNameWithoutSuffix = "Log";
/// <summary>
/// 初始化生成器,设置语法过滤和代码生成逻辑
/// </summary>
/// <param name="context">增量生成器初始化上下文</param>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. 语法过滤:快速筛选候选类
var targets = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) =>
{
if (node is not ClassDeclarationSyntax cls) return false;
// 只要包含 Log 字眼的 Attribute 就先放行
return cls.AttributeLists.SelectMany(a => a.Attributes).Any(a =>
{
var name = a.Name.ToString();
// 简单的字符串匹配,防止错过别名情况
return name.Contains(AttributeShortNameWithoutSuffix);
});
},
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);
// 2. 生成代码
context.RegisterSourceOutput(targets, (spc, pair) =>
{
try
{
var classDecl = pair.ClassDecl;
var classSymbol = pair.Symbol!;
// 再次确认是否真的含有目标 Attribute (语义检查)
var attr = GetAttribute(classSymbol);
if (attr == null) return; // 可能是名字相似但不是我们要的 Attribute
// 检查 partial
if (!classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
spc.ReportDiagnostic(Diagnostic.Create(
Diagnostics.MustBePartial,
classDecl.Identifier.GetLocation(),
classSymbol.Name));
return;
}
var source = Generate(classSymbol, attr);
var hintName = $"{classSymbol.Name}.Logger.g.cs";
spc.AddSource(hintName, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
// === 关键修复:生成错误报告文件 ===
var errorSource = $"// source generator error: {ex.Message}\n// StackTrace:\n// {ex.StackTrace}";
// 替换非法字符以防文件名报错
var safeName = pair.Symbol?.Name ?? "Unknown";
spc.AddSource($"{safeName}.Logger.Error.g.cs", SourceText.From(errorSource, Encoding.UTF8));
}
});
}
/// <summary>
/// 获取类符号上的LogAttribute特性
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <returns>LogAttribute特性数据如果不存在则返回null</returns>
private static AttributeData? GetAttribute(INamedTypeSymbol classSymbol)
{
return classSymbol.GetAttributes().FirstOrDefault(a =>
{
var cls = a.AttributeClass;
if (cls == null) return false;
// 宽松匹配:全名匹配 OR 名字匹配
return cls.ToDisplayString() == AttributeMetadataName ||
cls.Name == AttributeShortName;
});
}
/// <summary>
/// 生成日志字段代码
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <param name="attr">LogAttribute特性数据</param>
/// <returns>生成的C#代码字符串</returns>
private static string Generate(INamedTypeSymbol classSymbol, AttributeData attr)
{
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace
? null
: classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
// === 解析 Name ===
var name = className; // 默认使用类名
// 检查是否有构造函数参数
if (attr.ConstructorArguments.Length > 0)
{
var argValue = attr.ConstructorArguments[0].Value;
name = argValue switch
{
// 情况 1: 参数存在,但值为 null (例如 [Log] 且构造函数有默认值 null)
null => className,
// 情况 2: 参数存在,且是有效的字符串 (例如 [Log("MyCategory")])
string s when !string.IsNullOrWhiteSpace(s) => s,
_ => $"{className}_InvalidArg"
};
}
// === 解析 Named Arguments (更加安全的获取方式) ===
var fieldName = GetNamedArg(attr, "FieldName")?.ToString() ?? "_log";
var access =
GetNamedArg(attr, "AccessModifier")?.ToString() ??
"private"; // 注意:如果你的 AccessModifier 是枚举,这里得到的可能是 int 或枚举名
// 处理 bool 类型
var isStaticObj = GetNamedArg(attr, "IsStatic");
var isStatic = isStaticObj is not bool b || b; // 默认为 true
var staticKeyword = isStatic ? "static " : "";
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using GFramework.Core.logging;"); // 确保这里引用了 ILog 和 Log 类
if (ns is not null)
{
sb.AppendLine($"namespace {ns}");
sb.AppendLine("{");
}
sb.AppendLine($" public partial class {className}");
sb.AppendLine(" {");
sb.AppendLine($" /// <summary>Auto-generated logger</summary>");
sb.AppendLine(
$" {access} {staticKeyword}readonly ILogger {fieldName} = " +
$"new ConsoleLoggerFactory.GetLogger(\"{name}\");");
sb.AppendLine(" }");
if (ns is not null)
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// 从特性数据中获取命名参数的值
/// </summary>
/// <param name="attr">特性数据</param>
/// <param name="name">参数名称</param>
/// <returns>参数值如果不存在则返回null</returns>
private static object? GetNamedArg(AttributeData attr, string name)
{
return (from kv in attr.NamedArguments where kv.Key == name select kv.Value.Value).FirstOrDefault();
}
}
}

View File

@ -1,443 +0,0 @@
# GFramework 日志代码生成器 (LogAttribute)
## 概述
GFramework 提供了一个强大的日志代码生成器,类似于 Java 的 `@Slf4j` 注解。通过在类上使用 `[Log]` 特性,编译器会自动为该类生成一个日志记录器字段,让您在类的任何地方都能方便地使用日志记录功能。
## 快速开始
### 1. 基本使用
在类上添加 `[Log]` 特性:
```csharp
using GFramework.Generator.Attributes.generator.logging;
[Log]
public partial class MyService
{
public void DoSomething()
{
// 自动生成的 Log 字段可以直接使用
Log.Info("开始执行操作");
try
{
// 业务逻辑
Log.Debug("执行业务逻辑", new { Operation = "DoSomething" });
Log.Info("操作执行成功");
}
catch (Exception ex)
{
Log.Error("操作执行失败", ex);
}
}
}
```
编译后,生成器会自动为该类添加:
```csharp
public partial class MyService
{
private static ILog Log = Log.CreateLogger("MyService");
}
```
### 2. 自定义类别名称
```csharp
[Log("CustomService")]
public partial class MyService
{
// 生成的 logger 类别为 "CustomService" 而不是 "MyService"
}
```
### 3. 自定义字段配置
```csharp
[Log(FieldName = "Logger", AccessModifier = "protected", IsStatic = false)]
public partial class MyService
{
// 生成: protected ILog Logger = Log.CreateLogger("MyService");
}
```
## 特性参数说明
### LogAttribute 构造函数参数
- **category** (string, 可选): 指定日志类别,默认为类名
### 命名参数
- **FieldName** (string, 默认 "Log"): 指定生成的字段名称
- **AccessModifier** (string, 默认 "private"): 指定字段的访问修饰符
- **IsStatic** (bool, 默认 true): 指定字段是否为静态的
## 使用示例
### 在系统中的使用
```csharp
[Log("System")]
public partial class GameSystem : AbstractSystem
{
protected override void OnInit()
{
Log.Info("GameSystem 初始化开始");
// 初始化逻辑
Log.Debug("正在加载游戏数据...");
Log.Info("GameSystem 初始化完成");
}
protected override void OnUpdate(float deltaTime)
{
Log.Trace("系统更新", new { DeltaTime = deltaTime });
}
protected override void OnDestroy()
{
Log.Info("GameSystem 销毁");
}
}
```
### 在模型中的使用
```csharp
[Log("Model")]
public partial class UserModel : AbstractModel
{
public string UserName { get; set; }
public void SetUserName(string userName)
{
Log.Debug("设置用户名", new { OldValue = UserName, NewValue = userName });
UserName = userName;
Log.Info("用户名设置完成", new { UserName = userName });
}
}
```
### 在工具类中的使用
```csharp
[Log("Utility")]
public partial class FileUtility
{
public void SaveFile(string filePath, string content)
{
Log.Debug("开始保存文件", new { FilePath = filePath });
try
{
// 文件保存逻辑
Log.Info("文件保存成功", new { FilePath = filePath });
}
catch (Exception ex)
{
Log.Error("文件保存失败", ex, new { FilePath = filePath });
}
}
}
```
### 网络服务中的使用
```csharp
[Log("Network")]
public partial class NetworkService
{
public async Task<string> GetDataAsync(string url)
{
Log.Debug("发起网络请求", new { Url = url, Method = "GET" });
try
{
var response = await httpClient.GetStringAsync(url);
Log.Info("网络请求成功", new { Url = url, ResponseLength = response.Length });
return response;
}
catch (Exception ex)
{
Log.Error("网络请求失败", ex, new { Url = url });
throw;
}
}
}
```
### 数据库服务中的使用
```csharp
[Log("Database")]
public partial class DatabaseService
{
public async Task<User> GetUserAsync(int userId)
{
Log.Debug("查询用户信息", new { UserId = userId });
try
{
var user = await _dbContext.Users.FindAsync(userId);
if (user != null)
{
Log.Info("用户查询成功", new { UserId = userId, UserName = user.Name });
}
else
{
Log.Warn("用户不存在", new { UserId = userId });
}
return user;
}
catch (Exception ex)
{
Log.Error("用户查询失败", ex, new { UserId = userId });
throw;
}
}
}
```
## 高级配置示例
### 使用静态字段
```csharp
[Log(IsStatic = true)] // 默认配置
public partial class StaticService
{
public static void StaticMethod()
{
Log.Info("静态方法调用");
}
}
```
### 使用实例字段
```csharp
[Log(IsStatic = false)]
public partial class InstanceService
{
private readonly string _instanceId;
public InstanceService(string instanceId)
{
_instanceId = instanceId;
}
public void InstanceMethod()
{
Log.Info("实例方法调用", new { InstanceId = _instanceId });
}
}
```
### 使用受保护的字段
```csharp
[Log(AccessModifier = "protected")]
public partial class BaseService
{
// 子类可以访问 protected 字段
}
[Log] // 派生类也可以有自己的日志记录器
public partial class DerivedService : BaseService
{
public void DerivedMethod()
{
Log.Info("派生类方法");
// 也可以访问基类的 protected Log 字段
}
}
```
## 最佳实践
### 1. 使用合适的日志类别名称
```csharp
// 好的做法:使用有意义的类别名称
[Log("UserService")]
public partial class UserService { }
// 避免:使用过于通用的类别名称
[Log("Service")]
public partial class UserService { }
```
### 2. 合理设置日志级别
```csharp
[Log("BusinessLogic")]
public partial class BusinessService
{
public void ProcessOrder(Order order)
{
// 使用 Info 记录重要的业务流程
Log.Info("开始处理订单", new { OrderId = order.Id });
// 使用 Debug 记录调试信息
Log.Debug("订单验证通过", new { OrderId = order.Id });
// 使用 Warning 记录异常情况
if (order.Amount > 10000)
{
Log.Warn("大额订单", new { OrderId = order.Id, Amount = order.Amount });
}
// 使用 Error 记录错误
try
{
// 业务逻辑
}
catch (Exception ex)
{
Log.Error("订单处理失败", ex, new { OrderId = order.Id });
}
}
}
```
### 3. 记录上下文信息
```csharp
[Log("UserManagement")]
public partial class UserManager
{
public void UpdateUserProfile(int userId, UserProfile profile)
{
Log.Info("更新用户资料", new
{
UserId = userId,
Changes = profile.GetChanges(),
Timestamp = DateTime.Now
});
}
}
```
### 4. 在异常处理中使用日志
```csharp
[Log("DataAccess")]
public partial class UserRepository
{
public async Task<User> GetUserAsync(int userId)
{
try
{
return await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
}
catch (DbException ex)
{
Log.Error("数据库查询失败", ex, new { UserId = userId });
throw new DataAccessException("无法获取用户信息", ex);
}
}
}
```
## 框架组件集成
### 在 GFramework 架构中使用
```csharp
[Log("Architecture")]
public partial class ExampleArchitecture : AbstractArchitecture
{
protected override void Init()
{
Log.Info("ExampleArchitecture 初始化开始");
// 注册系统
RegisterSystem(new GameSystem());
RegisterSystem(new UISystem());
// 注册模型
RegisterModel(new UserModel());
RegisterModel(new GameModel());
// 注册工具
RegisterUtility(new FileUtility());
Log.Info("ExampleArchitecture 初始化完成");
}
}
```
### 在事件处理中使用
```csharp
[Log("Event")]
public partial class UserEventHandler
{
[Log("EventHandler")]
public void OnUserLoginEvent(UserLoginEvent evt)
{
Log.Info("用户登录事件", new
{
UserId = evt.UserId,
LoginTime = evt.LoginTime,
IpAddress = evt.IpAddress
});
}
[Log("EventHandler")]
public void OnUserLogoutEvent(UserLogoutEvent evt)
{
Log.Info("用户登出事件", new
{
UserId = evt.UserId,
LogoutTime = evt.LogoutTime,
SessionDuration = evt.SessionDuration
});
}
}
```
## 注意事项
1. **部分类**: 使用 `[Log]` 特性的类必须是 `partial` 类,因为生成器会添加字段到同一个类中。
2. **命名空间引用**: 确保项目中引用了 `GFramework.Generator.Attributes` 包。
3. **编译时生成**: 日志字段是在编译时生成的,不会影响运行时性能。
4. **多线程安全**: 生成的 `ILog` 实例是线程安全的,可以在多线程环境中使用。
5. **继承**: 派生类可以有自己的 `[Log]` 特性,也可以访问基类的受保护日志字段。
## 性能考虑
- **编译时生成**: 字段在编译时生成,没有运行时开销
- **延迟初始化**: 日志记录器按需创建,避免不必要的对象创建
- **级别检查**: 生成的代码包含级别检查,性能与手动创建的日志记录器相同
## 与现有日志系统的兼容性
生成的代码完全兼容现有的 `GFramework.Core.logging` 系统:
```csharp
// 现有的方式仍然可以使用
var manualLogger = Log.CreateLogger("Manual");
// 新生成的字段也可以正常使用
[Log]
public partial class MyService
{
public void Method()
{
// 两种方式都可以使用
manualLogger.Info("手动创建的日志");
Log.Info("自动生成的日志");
}
}
```
通过使用 `[Log]` 特性,您可以显著减少样板代码,提高开发效率,同时保持日志记录的一致性和灵活性。

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,37 @@
#nullable enable
namespace GFramework.SourceGenerators.Attributes.logging;
/// <summary>
/// Godot日志特性用于在类上标记以自动生成日志字段
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class GodotLogAttribute : Attribute
{
/// <summary>
/// 初始化 GodotLogAttribute 类的新实例
/// </summary>
public GodotLogAttribute()
{
}
/// <summary>
/// 初始化 GodotLogAttribute 类的新实例
/// </summary>
/// <param name="name">日志分类名</param>
public GodotLogAttribute(string? name)
{
Name = name;
}
/// <summary>日志分类名(默认使用类名)</summary>
public string? Name { get; set; }
/// <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";
}

View File

@ -0,0 +1,9 @@
; 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.Godot.Logging | Error | GodotLoggerDiagnostics
GFW_LOG001 | GFramework.Godot.Logging | Warning | GodotLoggerDiagnostics

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md"/>
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
using Microsoft.CodeAnalysis;
namespace GFramework.Godot.SourceGenerators.logging;
/// <summary>
/// 提供诊断描述符的静态类用于GFramework日志生成器的编译时检查
/// </summary>
internal static class GodotLoggerDiagnostics
{
/// <summary>
/// 诊断描述符:标识使用[GodotLog]特性的类必须声明为partial
/// </summary>
/// <remarks>
/// ID: GFLOG001
/// 严重性: Error
/// 分类: GFramework.Godot.Logging
/// </remarks>
public static readonly DiagnosticDescriptor MustBePartial =
new(
id: "GFLOG001",
title: "Class must be partial",
messageFormat: "Class '{0}' must be declared as partial to use [GodotLog]",
category: "GFramework.Godot.Logging",
DiagnosticSeverity.Error,
isEnabledByDefault: true
);
/// <summary>
/// 诊断描述符标识GodotLogAttribute无法在指定类上生成Logger
/// </summary>
/// <remarks>
/// ID: GFW_LOG001
/// 严重性: Warning
/// 分类: GFramework.Godot.Logging
/// </remarks>
public static readonly DiagnosticDescriptor LogAttributeInvalid = new(
id: "GFW_LOG001",
title: "GodotLogAttribute cannot generate Logger",
messageFormat: "GodotLogAttribute on class '{0}' is ineffective: {1}",
category: "GFramework.Godot.Logging",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}

View File

@ -0,0 +1,181 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace GFramework.Godot.SourceGenerators.logging;
/// <summary>
/// 日志生成器用于为标记了LogAttribute的类自动生成日志字段
/// </summary>
[Generator]
public sealed class GodotLoggerGenerator : IIncrementalGenerator
{
private const string AttributeMetadataName = "GFramework.SourceGenerators.Attributes.logging.GodotLogAttribute";
private const string AttributeShortName = "GodotLogAttribute";
private const string AttributeShortNameWithoutSuffix = "Log";
/// <summary>
/// 初始化生成器,设置语法过滤和代码生成逻辑
/// </summary>
/// <param name="context">增量生成器初始化上下文</param>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. 语法过滤:快速筛选候选类
var targets = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) =>
{
if (node is not ClassDeclarationSyntax cls) return false;
// 只要包含 Log 字眼的 Attribute 就先放行
return cls.AttributeLists.SelectMany(a => a.Attributes).Any(a =>
{
var name = a.Name.ToString();
// 简单的字符串匹配,防止错过别名情况
return name.Contains(AttributeShortNameWithoutSuffix);
});
},
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);
// 2. 生成代码
context.RegisterSourceOutput(targets, (spc, pair) =>
{
try
{
var classDecl = pair.ClassDecl;
var classSymbol = pair.Symbol!;
// 再次确认是否真的含有目标 Attribute (语义检查)
var attr = GetAttribute(classSymbol);
if (attr == null) return; // 可能是名字相似但不是我们要的 Attribute
// 检查 partial
if (!classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
spc.ReportDiagnostic(Diagnostic.Create(
GodotLoggerDiagnostics.MustBePartial,
classDecl.Identifier.GetLocation(),
classSymbol.Name));
return;
}
var source = Generate(classSymbol, attr);
var hintName = $"{classSymbol.Name}.Logger.g.cs";
spc.AddSource(hintName, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
// === 关键修复:生成错误报告文件 ===
var errorSource = $"// source generator error: {ex.Message}\n// StackTrace:\n// {ex.StackTrace}";
// 替换非法字符以防文件名报错
var safeName = pair.Symbol?.Name ?? "Unknown";
spc.AddSource($"{safeName}.Logger.Error.g.cs", SourceText.From(errorSource, Encoding.UTF8));
}
});
}
/// <summary>
/// 获取类符号上的LogAttribute特性
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <returns>LogAttribute特性数据如果不存在则返回null</returns>
private static AttributeData? GetAttribute(INamedTypeSymbol classSymbol)
{
return classSymbol.GetAttributes().FirstOrDefault(a =>
{
var cls = a.AttributeClass;
if (cls == null) return false;
// 宽松匹配:全名匹配 OR 名字匹配
return cls.ToDisplayString() == AttributeMetadataName ||
cls.Name == AttributeShortName;
});
}
/// <summary>
/// 生成日志字段代码
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <param name="attr">LogAttribute特性数据</param>
/// <returns>生成的C#代码字符串</returns>
private static string Generate(INamedTypeSymbol classSymbol, AttributeData attr)
{
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace
? null
: classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
// === 解析 Name ===
var name = className; // 默认使用类名
// 检查是否有构造函数参数
if (attr.ConstructorArguments.Length > 0)
{
var argValue = attr.ConstructorArguments[0].Value;
name = argValue switch
{
// 情况 1: 参数存在,但值为 null (例如 [GodotLog] 且构造函数有默认值 null)
null => className,
// 情况 2: 参数存在,且是有效的字符串 (例如 [GodotLog("MyCategory")])
string s when !string.IsNullOrWhiteSpace(s) => s,
_ => $"{className}_InvalidArg"
};
}
// === 解析 Named Arguments (更加安全的获取方式) ===
var fieldName = GetNamedArg(attr, "FieldName")?.ToString() ?? "_log";
var access =
GetNamedArg(attr, "AccessModifier")?.ToString() ??
"private"; // 注意:如果你的 AccessModifier 是枚举,这里得到的可能是 int 或枚举名
// 处理 bool 类型
var isStaticObj = GetNamedArg(attr, "IsStatic");
var isStatic = isStaticObj is not bool b || b; // 默认为 true
var staticKeyword = isStatic ? "static " : "";
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using GFramework.Core.logging;"); // 确保这里引用了 ILogger
sb.AppendLine("using GFramework.Godot.logging;"); // 确保这里引用了 GodotLoggerFactory
if (ns is not null)
{
sb.AppendLine($"namespace {ns}");
sb.AppendLine("{");
}
sb.AppendLine($" public partial class {className}");
sb.AppendLine(" {");
sb.AppendLine(" /// <summary>Auto-generated logger</summary>");
sb.AppendLine(
$" {access} {staticKeyword}readonly ILogger {fieldName} = " +
$"new GodotLoggerFactory.GetLogger(\"{name}\");");
sb.AppendLine(" }");
if (ns is not null)
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// 从特性数据中获取命名参数的值
/// </summary>
/// <param name="attr">特性数据</param>
/// <param name="name">参数名称</param>
/// <returns>参数值如果不存在则返回null</returns>
private static object? GetNamedArg(AttributeData attr, string name)
{
return (from kv in attr.NamedArguments where kv.Key == name select kv.Value.Value).FirstOrDefault();
}
}

View File

@ -1,6 +1,6 @@
using System;
namespace GFramework.Generator.Attributes.generator.enums
namespace GFramework.SourceGenerators.Attributes.enums
{
/// <summary>
/// 标注在 enum 上Source Generator 会为该 enum 生成扩展方法。

View File

@ -1,7 +1,7 @@
#nullable enable
using System;
namespace GFramework.Generator.Attributes.generator.logging;
namespace GFramework.SourceGenerators.Attributes.logging;
/// <summary>
/// 标注在类上Source Generator 会为该类自动生成一个日志记录器字段。
@ -9,6 +9,19 @@ namespace GFramework.Generator.Attributes.generator.logging;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class LogAttribute : Attribute
{
public LogAttribute()
{
}
/// <summary>
/// 初始化 GodotLogAttribute 类的新实例
/// </summary>
/// <param name="name">日志分类名,默认使用类名</param>
public LogAttribute(string? name)
{
Name = name;
}
/// <summary>日志分类名(默认使用类名)</summary>
public string? Name { get; set; }
@ -20,13 +33,4 @@ public sealed class LogAttribute : Attribute
/// <summary>访问修饰符</summary>
public string AccessModifier { get; set; } = "private";
public LogAttribute() { }
/// <summary>
/// 初始化 LogAttribute 类的新实例
/// </summary>
/// <param name="name">日志分类名,默认使用类名</param>
public LogAttribute(string? name)
{
Name = name;
}
}

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>GeWuYou.GFramework.Generator</PackageId>
<PackageId>GeWuYou.GFramework.SourceGenerators</PackageId>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynAnalyzer>true</IsRoslynAnalyzer>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
@ -26,7 +26,7 @@
<ItemGroup>
<!-- Generator 需要引用 Attributes 项目,但不作为运行时依赖 -->
<ProjectReference Include="..\GFramework.Generator.Attributes\GFramework.Generator.Attributes.csproj"
<ProjectReference Include="..\GFramework.SourceGenerators.Attributes\GFramework.SourceGenerators.Attributes.csproj"
PrivateAssets="all"/>
</ItemGroup>
@ -35,12 +35,8 @@
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false"/>
<None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="lib/netstandard2.0"/>
<None Include="$(OutputPath)\GFramework.Generator.Attributes.dll" Pack="true"
<None Include="$(OutputPath)\GFramework.SourceGenerators.Attributes.dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false"/>
<None Update="README.md">
<Pack>true</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -5,12 +5,13 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace GFramework.Generator.generator.enums;
namespace GFramework.SourceGenerators.enums;
[Generator]
public class EnumExtensionsGenerator : IIncrementalGenerator
{
private const string AttributeFullName = "GFramework.Generator.Attributes.generator.enums.GenerateEnumExtensionsAttribute";
private const string AttributeFullName =
"GFramework.SourceGenerators.Attributes.generator.enums.GenerateEnumExtensionsAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{

View File

@ -1,13 +1,12 @@
using Microsoft.CodeAnalysis;
namespace GFramework.Generator.generator.logging;
namespace GFramework.SourceGenerators.logging;
/// <summary>
/// 提供诊断描述符的静态类用于GFramework日志生成器的编译时检查
/// </summary>
internal static class Diagnostics
internal static class LoggerDiagnostics
{
/// <summary>
/// 定义诊断描述符:要求使用[Log]特性的类必须声明为partial
/// </summary>
@ -23,17 +22,16 @@ internal static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true
);
/// <summary>
/// 定义诊断描述符LogAttribute无法生成Logger的错误情况
/// </summary>
public static readonly DiagnosticDescriptor LogAttributeInvalid =
new(
id: "GFW_LOG001",
title: "LogAttribute 无法生成 Logger",
messageFormat: "类 '{0}' 上的 LogAttribute 无法生效:{1}",
category: "GFramework.Logging",
title: "LogAttribute cannot generate Logger",
messageFormat: "LogAttribute on class '{0}' is ineffective: {1}",
category: "GFramework.Godot.Logging",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
}

View File

@ -0,0 +1,185 @@
#nullable enable
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace GFramework.SourceGenerators.logging;
/// <summary>
/// 日志生成器用于为标记了LogAttribute的类自动生成日志字段
/// </summary>
[Generator]
public sealed class LoggerGenerator : IIncrementalGenerator
{
private const string AttributeMetadataName = "GFramework.SourceGenerators.Attributes.logging.GodotLogAttribute";
private const string AttributeShortName = "GodotLogAttribute";
private const string AttributeShortNameWithoutSuffix = "Log";
/// <summary>
/// 初始化生成器,设置语法过滤和代码生成逻辑
/// </summary>
/// <param name="context">增量生成器初始化上下文</param>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. 语法过滤:快速筛选候选类
var targets = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) =>
{
if (node is not ClassDeclarationSyntax cls) return false;
// 只要包含 Log 字眼的 Attribute 就先放行
return cls.AttributeLists.SelectMany(a => a.Attributes).Any(a =>
{
var name = a.Name.ToString();
// 简单的字符串匹配,防止错过别名情况
return name.Contains(AttributeShortNameWithoutSuffix);
});
},
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);
// 2. 生成代码
context.RegisterSourceOutput(targets, (spc, pair) =>
{
try
{
var classDecl = pair.ClassDecl;
var classSymbol = pair.Symbol!;
// 再次确认是否真的含有目标 Attribute (语义检查)
var attr = GetAttribute(classSymbol);
if (attr == null) return; // 可能是名字相似但不是我们要的 Attribute
// 检查 partial
if (!classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
spc.ReportDiagnostic(Diagnostic.Create(
LoggerDiagnostics.MustBePartial,
classDecl.Identifier.GetLocation(),
classSymbol.Name));
return;
}
var source = Generate(classSymbol, attr);
var hintName = $"{classSymbol.Name}.Logger.g.cs";
spc.AddSource(hintName, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
// === 关键修复:生成错误报告文件 ===
var errorSource = $"// source generator error: {ex.Message}\n// StackTrace:\n// {ex.StackTrace}";
// 替换非法字符以防文件名报错
var safeName = pair.Symbol?.Name ?? "Unknown";
spc.AddSource($"{safeName}.Logger.Error.g.cs", SourceText.From(errorSource, Encoding.UTF8));
}
});
}
/// <summary>
/// 获取类符号上的LogAttribute特性
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <returns>LogAttribute特性数据如果不存在则返回null</returns>
private static AttributeData? GetAttribute(INamedTypeSymbol classSymbol)
{
return classSymbol.GetAttributes().FirstOrDefault(a =>
{
var cls = a.AttributeClass;
if (cls == null) return false;
// 宽松匹配:全名匹配 OR 名字匹配
return cls.ToDisplayString() == AttributeMetadataName ||
cls.Name == AttributeShortName;
});
}
/// <summary>
/// 生成日志字段代码
/// </summary>
/// <param name="classSymbol">类符号</param>
/// <param name="attr">LogAttribute特性数据</param>
/// <returns>生成的C#代码字符串</returns>
private static string Generate(INamedTypeSymbol classSymbol, AttributeData attr)
{
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace
? null
: classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
// === 解析 Name ===
var name = className; // 默认使用类名
// 检查是否有构造函数参数
if (attr.ConstructorArguments.Length > 0)
{
var argValue = attr.ConstructorArguments[0].Value;
name = argValue switch
{
// 情况 1: 参数存在,但值为 null (例如 [Log] 且构造函数有默认值 null)
null => className,
// 情况 2: 参数存在,且是有效的字符串 (例如 [Log("MyCategory")])
string s when !string.IsNullOrWhiteSpace(s) => s,
_ => $"{className}_InvalidArg"
};
}
// === 解析 Named Arguments (更加安全的获取方式) ===
var fieldName = GetNamedArg(attr, "FieldName")?.ToString() ?? "_log";
var access =
GetNamedArg(attr, "AccessModifier")?.ToString() ??
"private"; // 注意:如果你的 AccessModifier 是枚举,这里得到的可能是 int 或枚举名
// 处理 bool 类型
var isStaticObj = GetNamedArg(attr, "IsStatic");
var isStatic = isStaticObj is not bool b || b; // 默认为 true
var staticKeyword = isStatic ? "static " : "";
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using GFramework.Core.logging;"); // 确保这里引用了 ILog 和 Log 类
if (ns is not null)
{
sb.AppendLine($"namespace {ns}");
sb.AppendLine("{");
}
sb.AppendLine($" public partial class {className}");
sb.AppendLine(" {");
sb.AppendLine($" /// <summary>Auto-generated logger</summary>");
sb.AppendLine(
$" {access} {staticKeyword}readonly ILogger {fieldName} = " +
$"new ConsoleLoggerFactory.GetLogger(\"{name}\");");
sb.AppendLine(" }");
if (ns is not null)
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// 从特性数据中获取命名参数的值
/// </summary>
/// <param name="attr">特性数据</param>
/// <param name="name">参数名称</param>
/// <returns>参数值如果不存在则返回null</returns>
private static object? GetNamedArg(AttributeData attr, string name)
{
return (from kv in attr.NamedArguments where kv.Key == name select kv.Value.Value).FirstOrDefault();
}
}

View File

@ -23,9 +23,27 @@
<None Include="README.md" Pack="true" PackagePath="" />
<None Remove="GFramework.Core\**" />
<None Remove="GFramework.Game\**" />
<None Remove="GFramework.Generator\**" />
<None Remove="GFramework.Generator.Attributes\**" />
<None Remove="GFramework.Godot\**" />
<None Update="GFramework.SourceGenerators\bin\Debug\netstandard2.0\GFramework.Generator.Attributes.dll">
<Link>GFramework.SorceGenerators\bin\Debug\netstandard2.0\GFramework.Generator.Attributes.dll</Link>
</None>
<None Update="GFramework.SourceGenerators\logging\README.md">
<Link>GFramework.SorceGenerators\logging\README.md</Link>
</None>
<None Update="GFramework.SourceGenerators\README.md">
<Link>GFramework.SorceGenerators\README.md</Link>
</None>
<None Update="GFramework.SourceGenerators\AnalyzerReleases.Shipped.md">
<Link>GFramework.SorceGenerators\AnalyzerReleases.Shipped.md</Link>
</None>
<None Update="GFramework.SourceGenerators\AnalyzerReleases.Unshipped.md">
<Link>GFramework.SorceGenerators\AnalyzerReleases.Unshipped.md</Link>
</None>
<None Remove="GFramework.Godot.SourceGenerators\**"/>
<None Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
<None Remove="GFramework.SorceGenerators\**"/>
<None Remove="GFramework.SourceGenerators\**"/>
<None Remove="GFramework.SourceGenerators.Attributes\**"/>
</ItemGroup>
<!-- 聚合核心模块 -->
<ItemGroup>
@ -35,15 +53,30 @@
<ItemGroup>
<Compile Remove="GFramework.Core\**" />
<Compile Remove="GFramework.Game\**" />
<Compile Remove="GFramework.Generator\**" />
<Compile Remove="GFramework.Generator.Attributes\**" />
<Compile Remove="GFramework.Godot\**" />
<Compile Update="GFramework.SourceGenerators\enums\EnumExtensionsGenerator.cs">
<Link>GFramework.SorceGenerators\enums\EnumExtensionsGenerator.cs</Link>
</Compile>
<Compile Update="GFramework.SourceGenerators\logging\Diagnostic.cs">
<Link>GFramework.SorceGenerators\logging\Diagnostic.cs</Link>
</Compile>
<Compile Update="GFramework.SourceGenerators\logging\LoggerGenerator.cs">
<Link>GFramework.SorceGenerators\logging\LoggerGenerator.cs</Link>
</Compile>
<Compile Remove="GFramework.Godot.SourceGenerators\**"/>
<Compile Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
<Compile Remove="GFramework.SorceGenerators\**"/>
<Compile Remove="GFramework.SourceGenerators\**"/>
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="GFramework.Core\**" />
<EmbeddedResource Remove="GFramework.Game\**" />
<EmbeddedResource Remove="GFramework.Generator\**" />
<EmbeddedResource Remove="GFramework.Generator.Attributes\**" />
<EmbeddedResource Remove="GFramework.Godot\**" />
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators\**"/>
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
<EmbeddedResource Remove="GFramework.SorceGenerators\**"/>
<EmbeddedResource Remove="GFramework.SourceGenerators\**"/>
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
</ItemGroup>
</Project>

View File

@ -2,9 +2,9 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework ", "GFramework.csproj", "{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Generator", "GFramework.Generator\GFramework.Generator.csproj", "{E9D51809-0351-4B83-B85B-B5F469AAB3B8}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.SourceGenerators", "GFramework.SourceGenerators\GFramework.SourceGenerators.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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.SourceGenerators.Attributes", "GFramework.SourceGenerators.Attributes\GFramework.SourceGenerators.Attributes.csproj", "{84C5C3C9-5620-4924-BA04-92F813F2B70F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Core", "GFramework.Core\GFramework.Core.csproj", "{A6D5854D-79EA-487A-9ED9-396E6A1F8031}"
EndProject
@ -12,6 +12,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot", "GFramew
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game", "GFramework.Game\GFramework.Game.csproj", "{0B00816B-E8B2-4562-8C11-0C06CE761638}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators", "GFramework.Godot.SourceGenerators\GFramework.Godot.SourceGenerators.csproj", "{C56FD287-CBC6-4C44-B3DF-103FA3660CA0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators.Attributes", "GFramework.Godot.SourceGenerators.Attributes\GFramework.Godot.SourceGenerators.Attributes.csproj", "{3A1132B7-EC3B-4BB6-A752-8ADC92BC08A0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -42,5 +46,13 @@ Global
{0B00816B-E8B2-4562-8C11-0C06CE761638}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B00816B-E8B2-4562-8C11-0C06CE761638}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B00816B-E8B2-4562-8C11-0C06CE761638}.Release|Any CPU.Build.0 = Release|Any CPU
{C56FD287-CBC6-4C44-B3DF-103FA3660CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C56FD287-CBC6-4C44-B3DF-103FA3660CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C56FD287-CBC6-4C44-B3DF-103FA3660CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C56FD287-CBC6-4C44-B3DF-103FA3660CA0}.Release|Any CPU.Build.0 = Release|Any CPU
{3A1132B7-EC3B-4BB6-A752-8ADC92BC08A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3A1132B7-EC3B-4BB6-A752-8ADC92BC08A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3A1132B7-EC3B-4BB6-A752-8ADC92BC08A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A1132B7-EC3B-4BB6-A752-8ADC92BC08A0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal