refactor(source-generators): 提取通用诊断描述符并更新代码生成器

- 将 ClassMustBePartial 诊断描述符移至 CommonDiagnostics 类
- 更新 GodotLoggerGenerator 和 LoggerGenerator 引用通用诊断
- 添加 ContextAwareGenerator 实现上下文感知功能
- 创建 ContextAwareAttribute 标记需要自动实现 IContextAware 的类
- 在项目中添加对 GFramework.SourceGenerators.Common 的引用
- 更新诊断规则 ID 命名规范
- 修复 AnalyzerReleases 文件格式
- 添加 nullable enable 配置
- 在解决方案文件中添加新项目引用
This commit is contained in:
GwWuYou 2025-12-26 22:05:34 +08:00
parent 017870421e
commit a888e76842
17 changed files with 218 additions and 52 deletions

View File

@ -3,7 +3,6 @@
### New Rules
Rule ID | Category | Severity | Notes
------------|--------------------------|----------|------------------------
GFLOG001 | GFramework.Godot.Logging | Error | GodotLoggerDiagnostics
GFW_LOG001 | GFramework.Godot.Logging | Warning | GodotLoggerDiagnostics
Rule ID | Category | Severity | Notes
----------------------|--------------------------|----------|------------------------
GF_Godot_Logging_001 | GFramework.Godot.Logging | Warning | GodotLoggerDiagnostics

View File

@ -23,15 +23,11 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md"/>
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md"/>
</ItemGroup>
<ItemGroup>
<!-- Generator 需要引用 Attributes 项目,但不作为运行时依赖 -->
<ProjectReference Include="..\GFramework.Godot.SourceGenerators.Attributes\GFramework.Godot.SourceGenerators.Attributes.csproj"
PrivateAssets="all"/>
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
</ItemGroup>
<!-- 将 Generator 和 Attributes DLL 打包为 Analyzer -->
<ItemGroup>
@ -41,4 +37,8 @@
<None Include="$(OutputPath)\GFramework.Godot.SourceGenerators.Attributes.dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md"/>
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md"/>
</ItemGroup>
</Project>

View File

@ -7,25 +7,6 @@ namespace GFramework.Godot.SourceGenerators.logging;
/// </summary>
internal static class GodotLoggerDiagnostics
{
/// <summary>
/// 诊断描述符:标识使用[GodotLog]特性的类必须声明为partial
/// </summary>
/// <remarks>
/// ID: GFLOG001
/// 严重性: Error
/// 分类: GFramework.Godot.Logging
/// </remarks>
public static readonly DiagnosticDescriptor MustBePartial =
new(
"GFLOG001",
"Class must be partial",
"Class '{0}' must be declared as partial to use [GodotLog]",
"GFramework.Godot.Logging",
DiagnosticSeverity.Error,
true
);
/// <summary>
/// 诊断描述符标识GodotLogAttribute无法在指定类上生成Logger
/// </summary>
@ -35,7 +16,7 @@ internal static class GodotLoggerDiagnostics
/// 分类: GFramework.Godot.Logging
/// </remarks>
public static readonly DiagnosticDescriptor LogAttributeInvalid = new(
"GFW_LOG001",
"GF_Godot_Logging_001",
"GodotLogAttribute cannot generate Logger",
"GodotLogAttribute on class '{0}' is ineffective: {1}",
"GFramework.Godot.Logging",

View File

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Text;
using GFramework.SourceGenerators.Common.diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -63,7 +64,7 @@ public sealed class GodotLoggerGenerator : IIncrementalGenerator
if (!classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
spc.ReportDiagnostic(Diagnostic.Create(
GodotLoggerDiagnostics.MustBePartial,
CommonDiagnostics.ClassMustBePartial,
classDecl.Identifier.GetLocation(),
classSymbol.Name));

View File

@ -0,0 +1,11 @@
using System;
namespace GFramework.SourceGenerators.Attributes.rule;
/// <summary>
/// 标记该类需要自动实现 IContextAware
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ContextAwareAttribute : Attribute
{
}

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
---------|-------------------|----------|-------------------
GFC001 | GFramework.Common | Error | CommonDiagnostics

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md"/>
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,29 @@
using Microsoft.CodeAnalysis;
namespace GFramework.SourceGenerators.Common.diagnostics;
/// <summary>
/// 提供通用诊断描述符的静态类
/// </summary>
public static class CommonDiagnostics
{
/// <summary>
/// 定义类必须为partial的诊断描述符
/// </summary>
/// <remarks>
/// 诊断ID: GF001
/// 诊断消息: "Class '{0}' must be declared partial for code generation"
/// 分类: GFramework.Common
/// 严重性: Error
/// 是否启用: true
/// </remarks>
public static readonly DiagnosticDescriptor ClassMustBePartial =
new(
"GFC001",
"Class must be partial",
"Class '{0}' must be declared partial for code generation",
"GFramework.Common",
DiagnosticSeverity.Error,
true
);
}

View File

@ -3,7 +3,6 @@
### New Rules
| Rule ID | Category | Severity | Notes |
|------------|--------------------|----------|-------------|
| GFLOG001 | GFramework.Logging | Error | Diagnostics |
| GFW_LOG001 | GFramework.Logging | Warning | Diagnostics |
Rule ID | Category | Severity | Notes
----------------|--------------------------|----------|-------------------
GF_Logging_001 | GFramework.Godot.Logging | Warning | LoggerDiagnostics

View File

@ -16,6 +16,7 @@
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all"/>
@ -27,6 +28,7 @@
<!-- Generator 需要引用 Attributes 项目,但不作为运行时依赖 -->
<ProjectReference Include="..\GFramework.SourceGenerators.Attributes\GFramework.SourceGenerators.Attributes.csproj"
PrivateAssets="all"/>
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
</ItemGroup>
<!-- 将 Generator 和 Attributes DLL 打包为 Analyzer -->

View File

@ -7,28 +7,12 @@ namespace GFramework.SourceGenerators.logging;
/// </summary>
internal static class LoggerDiagnostics
{
/// <summary>
/// 定义诊断描述符:要求使用[Log]特性的类必须声明为partial
/// </summary>
/// <remarks>
/// 当类使用[Log]特性但未声明为partial时编译器将报告此错误
/// </remarks>
public static readonly DiagnosticDescriptor MustBePartial =
new(
"GFLOG001",
"Class must be partial",
"Class '{0}' must be declared partial to use [Log]",
"GFramework.Logging",
DiagnosticSeverity.Error,
true
);
/// <summary>
/// 定义诊断描述符LogAttribute无法生成Logger的错误情况
/// </summary>
public static readonly DiagnosticDescriptor LogAttributeInvalid =
new(
"GFW_LOG001",
"GF_Logging_001",
"LogAttribute cannot generate Logger",
"LogAttribute on class '{0}' is ineffective: {1}",
"GFramework.Godot.Logging",

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Text;
using GFramework.SourceGenerators.Common.diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -64,7 +65,7 @@ public sealed class LoggerGenerator : IIncrementalGenerator
if (!classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
spc.ReportDiagnostic(Diagnostic.Create(
LoggerDiagnostics.MustBePartial,
CommonDiagnostics.ClassMustBePartial,
classDecl.Identifier.GetLocation(),
classSymbol.Name));

View File

@ -0,0 +1,114 @@
using System.Linq;
using System.Text;
using GFramework.SourceGenerators.Common.diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace GFramework.SourceGenerators.rule;
[Generator]
public sealed class ContextAwareGenerator : IIncrementalGenerator
{
private const string AttributeMetadataName =
"GFramework.SourceGenerators.Attributes.rule.ContextAwareAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. 找到所有 class 声明
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => GetCandidate(ctx)
)
.Where(static s => s is not null);
// 2. 生成代码
context.RegisterSourceOutput(
classDeclarations,
static (spc, source) => Generate(spc, source!)
);
}
private static INamedTypeSymbol? GetCandidate(GeneratorSyntaxContext context)
{
var classDecl = (ClassDeclarationSyntax)context.Node;
if (classDecl.AttributeLists.Count == 0)
return null;
if (context.SemanticModel.GetDeclaredSymbol(classDecl)
is not { } symbol)
return null;
return Enumerable.Any(symbol.GetAttributes(),
attr => attr.AttributeClass?.ToDisplayString() == AttributeMetadataName)
? symbol
: null;
}
private static void Generate(
SourceProductionContext context,
INamedTypeSymbol symbol)
{
var syntax = symbol.DeclaringSyntaxReferences
.FirstOrDefault()?
.GetSyntax() as ClassDeclarationSyntax;
if (syntax is null || !syntax.Modifiers.Any(SyntaxKind.PartialKeyword))
{
context.ReportDiagnostic(
Diagnostic.Create(
CommonDiagnostics.ClassMustBePartial,
syntax?.Identifier.GetLocation(),
symbol.Name
)
);
return;
}
var ns = symbol.ContainingNamespace.IsGlobalNamespace
? null
: symbol.ContainingNamespace.ToDisplayString();
var source = GenerateSource(ns, symbol);
context.AddSource(
$"{symbol.Name}.ContextAware.g.cs",
source
);
}
private static string GenerateSource(string? ns, INamedTypeSymbol symbol)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable");
if (ns is not null)
{
sb.AppendLine($"namespace {ns};");
sb.AppendLine();
}
sb.AppendLine($"partial class {symbol.Name} : GFramework.Core.rule.IContextAware");
sb.AppendLine("{");
sb.AppendLine(
" protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!;");
sb.AppendLine("""
void GFramework.Core.rule.IContextAware.SetContext(
GFramework.Core.architecture.IArchitectureContext context)
{
Context = context;
}
""");
sb.AppendLine("}");
return sb.ToString();
}
}

View File

@ -44,6 +44,7 @@
<None Remove="GFramework.SorceGenerators\**"/>
<None Remove="GFramework.SourceGenerators\**"/>
<None Remove="GFramework.SourceGenerators.Attributes\**"/>
<None Remove="GFramework.SourceGenerators.Common\**"/>
</ItemGroup>
<!-- 聚合核心模块 -->
<ItemGroup>
@ -68,6 +69,7 @@
<Compile Remove="GFramework.SorceGenerators\**"/>
<Compile Remove="GFramework.SourceGenerators\**"/>
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
<Compile Remove="GFramework.SourceGenerators.Common\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="GFramework.Core\**"/>
@ -78,5 +80,13 @@
<EmbeddedResource Remove="GFramework.SorceGenerators\**"/>
<EmbeddedResource Remove="GFramework.SourceGenerators\**"/>
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
<EmbeddedResource Remove="GFramework.SourceGenerators.Common\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
</ItemGroup>
</Project>

View File

@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGene
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.SourceGenerators.Common", "GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj", "{3DB57A3A-ACCF-47BE-A17B-2ADD68B6C8AA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -54,5 +56,9 @@ Global
{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
{3DB57A3A-ACCF-47BE-A17B-2ADD68B6C8AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3DB57A3A-ACCF-47BE-A17B-2ADD68B6C8AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3DB57A3A-ACCF-47BE-A17B-2ADD68B6C8AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3DB57A3A-ACCF-47BE-A17B-2ADD68B6C8AA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,2 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADiagnostic_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8fed4175e0a54d839582ab555761a2de4d5128_003Ff3_003Fd48d28bd_003FDiagnostic_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInputEventAction_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1c378f459c054fecaf4484a0fa6d44c055a800_003F18_003F33b52a1c_003FInputEventAction_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>