feat(rule): 添加上下文感知诊断规则

- 新增 GF_Rule_001 诊断规则,要求类必须实现 IContextAware 接口
- 创建 ContextAwareDiagnostic 类定义诊断规则元数据
- 修改 ContextAwareGenerator 实现 IContextAware 接口检查
- 优化生成器代码结构,添加候选类查找和输出生成功能
- 更新 AnalyzerReleases.Unshipped.md 文档
- 调整测试代码以适配新的诊断规则
- 修复日志诊断规则的命名空间大小写错误
This commit is contained in:
GeWuYou 2025-12-27 13:04:01 +08:00
parent eebd7de409
commit 40a9b523f5
6 changed files with 94 additions and 55 deletions

View File

@ -42,14 +42,6 @@ public class ContextAwareGeneratorTests
"""; """;
const string frameworkStub = """ const string frameworkStub = """
namespace GFramework.Core.rule
{
public interface IContextAware
{
void SetContext(GFramework.Core.architecture.IArchitectureContext context);
}
}
namespace GFramework.Core.architecture namespace GFramework.Core.architecture
{ {
public interface IArchitectureContext {} public interface IArchitectureContext {}
@ -61,7 +53,7 @@ public class ContextAwareGeneratorTests
#nullable enable #nullable enable
namespace TestApp; namespace TestApp;
partial class MyRule : GFramework.Core.rule.IContextAware partial class MyRule
{ {
protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!; protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!;
void GFramework.Core.rule.IContextAware.SetContext( void GFramework.Core.rule.IContextAware.SetContext(

View File

@ -3,6 +3,7 @@
### New Rules ### New Rules
Rule ID | Category | Severity | Notes Rule ID | Category | Severity | Notes
----------------|--------------------------|----------|------------------- ----------------|----------------------------------|----------|------------------------
GF_Logging_001 | GFramework.Godot.Logging | Warning | LoggerDiagnostics GF_Logging_001 | GFramework.Godot.Logging | Warning | LoggerDiagnostics
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic

View File

@ -15,7 +15,7 @@ internal static class LoggerDiagnostics
"GF_Logging_001", "GF_Logging_001",
"LogAttribute cannot generate Logger", "LogAttribute cannot generate Logger",
"LogAttribute on class '{0}' is ineffective: {1}", "LogAttribute on class '{0}' is ineffective: {1}",
"GFramework.Godot.Logging", "GFramework.Godot.logging",
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
true); true);
} }

View File

@ -0,0 +1,28 @@
using Microsoft.CodeAnalysis;
namespace GFramework.SourceGenerators.rule;
/// <summary>
/// 提供与上下文感知相关的诊断规则定义
/// </summary>
public static class ContextAwareDiagnostic
{
/// <summary>
/// 定义类必须实现IContextAware接口的诊断规则
/// </summary>
/// <remarks>
/// 诊断ID: GF_Rule_001
/// 诊断类别: GFramework.SourceGenerators.rule
/// 严重级别: 错误
/// 启用状态: true
/// 消息格式: "Class '{0}' must implement IContextAware"
/// </remarks>
public static readonly DiagnosticDescriptor ClassMustImplementIContextAware = new(
"GF_Rule_001",
"Class must implement IContextAware",
"Class '{0}' must implement IContextAware",
"GFramework.SourceGenerators.rule",
DiagnosticSeverity.Error,
true
);
}

View File

@ -15,75 +15,87 @@ public sealed class ContextAwareGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
// 1. 找到所有 class 声明 // 1️⃣ 查找候选类
var classDeclarations = context.SyntaxProvider var candidates = context.SyntaxProvider
.CreateSyntaxProvider( .CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax, predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => GetCandidate(ctx) transform: static (ctx, _) => GetCandidate(ctx)
) )
.Where(static s => s is not null); .Where(static s => s is not null);
// 2. 生成代码 // 2⃣ 注册生成输出
context.RegisterSourceOutput( context.RegisterSourceOutput(candidates, static (spc, symbol) =>
classDeclarations, {
static (spc, source) => Generate(spc, source!) if (symbol != null)
); GenerateOutput(spc, symbol);
});
} }
#region
private static INamedTypeSymbol? GetCandidate(GeneratorSyntaxContext context) private static INamedTypeSymbol? GetCandidate(GeneratorSyntaxContext context)
{ {
var classDecl = (ClassDeclarationSyntax)context.Node; if (context.SemanticModel.GetDeclaredSymbol(context.Node) is not INamedTypeSymbol symbol)
if (classDecl.AttributeLists.Count == 0)
return null; return null;
if (context.SemanticModel.GetDeclaredSymbol(classDecl) // 仅筛选带有 ContextAwareAttribute 的类
is not { } symbol) var hasAttr = symbol.GetAttributes()
return null; .Any(attr => attr.AttributeClass?.ToDisplayString() == AttributeMetadataName);
return Enumerable.Any(symbol.GetAttributes(), return hasAttr ? symbol : null;
attr => attr.AttributeClass?.ToDisplayString() == AttributeMetadataName)
? symbol
: null;
} }
#endregion
private static void Generate( #region +
SourceProductionContext context,
INamedTypeSymbol symbol) private static void GenerateOutput(SourceProductionContext context, INamedTypeSymbol symbol)
{ {
var syntax = symbol.DeclaringSyntaxReferences var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as ClassDeclarationSyntax;
.FirstOrDefault()? if (syntax == null)
.GetSyntax() as ClassDeclarationSyntax; return;
if (syntax is null || !syntax.Modifiers.Any(SyntaxKind.PartialKeyword)) // 1⃣ 必须是 partial
if (!syntax.Modifiers.Any(SyntaxKind.PartialKeyword))
{ {
context.ReportDiagnostic( context.ReportDiagnostic(Diagnostic.Create(
Diagnostic.Create( CommonDiagnostics.ClassMustBePartial,
CommonDiagnostics.ClassMustBePartial, syntax.Identifier.GetLocation(),
syntax?.Identifier.GetLocation(), symbol.Name
symbol.Name ));
)
);
return; return;
} }
// 2⃣ 必须实现 IContextAware直接或间接
if (!symbol.AllInterfaces.Any(i => i.ToDisplayString() == "GFramework.Core.rule.IContextAware"))
{
context.ReportDiagnostic(Diagnostic.Create(
ContextAwareDiagnostic.ClassMustImplementIContextAware,
syntax.Identifier.GetLocation(),
symbol.Name
));
return;
}
// 3⃣ 生成源码
var ns = symbol.ContainingNamespace.IsGlobalNamespace var ns = symbol.ContainingNamespace.IsGlobalNamespace
? null ? null
: symbol.ContainingNamespace.ToDisplayString(); : symbol.ContainingNamespace.ToDisplayString();
var source = GenerateSource(ns, symbol); var source = GenerateSource(ns, symbol);
context
context.AddSource( .AddSource(
$"{symbol.Name}.ContextAware.g.cs", $"{symbol.Name}.ContextAware.g.cs",
source source);
);
} }
#endregion
#region
private static string GenerateSource(string? ns, INamedTypeSymbol symbol) private static string GenerateSource(string? ns, INamedTypeSymbol symbol)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>"); sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable"); sb.AppendLine("#nullable enable");
@ -93,20 +105,20 @@ public sealed class ContextAwareGenerator : IIncrementalGenerator
sb.AppendLine(); sb.AppendLine();
} }
sb.AppendLine($"partial class {symbol.Name} : GFramework.Core.rule.IContextAware"); sb.AppendLine($"partial class {symbol.Name}");
sb.AppendLine("{"); sb.AppendLine("{");
sb.AppendLine( sb.AppendLine(
" protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!;"); " protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!;");
sb.AppendLine();
sb.AppendLine(" void GFramework.Core.rule.IContextAware.SetContext("); sb.AppendLine(" void GFramework.Core.rule.IContextAware.SetContext(");
sb.AppendLine(" GFramework.Core.architecture.IArchitectureContext context)"); sb.AppendLine(" GFramework.Core.architecture.IArchitectureContext context)");
sb.AppendLine(" {"); sb.AppendLine(" {");
sb.AppendLine(" Context = context;"); sb.AppendLine(" Context = context;");
sb.AppendLine(" }"); sb.AppendLine(" }");
sb.AppendLine("}"); sb.AppendLine("}");
return sb.ToString().TrimEnd(); return sb.ToString().TrimEnd();
} }
#endregion
} }

View File

@ -1,11 +1,17 @@
<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"> <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_003AAnalyzerTest_00601_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FSourcesCache_003Fe7a998cc7aa4af29968182b46ff292b3634fc9b5961db3d3d2a5291dc5a7843_003FAnalyzerTest_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAnalyzerTest_00601_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FSourcesCache_003Fe7a998cc7aa4af29968182b46ff292b3634fc9b5961db3d3d2a5291dc5a7843_003FAnalyzerTest_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<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_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_003ADiagnostic_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FLoacl_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> <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>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIVerifierExtensions_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FSourcesCache_003F3a7fb23d304bcd1dd4fcb38114e45d28a6a1446eb6b71c918bcb1d73a6cbf3_003FIVerifierExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIVerifierExtensions_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FSourcesCache_003F3a7fb23d304bcd1dd4fcb38114e45d28a6a1446eb6b71c918bcb1d73a6cbf3_003FIVerifierExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASolutionState_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FSourcesCache_003F59c43162311a41dc4e9a06273ab3fb541e6a64318777591f77584020ff865_003FSolutionState_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASolutionState_002Ecs_002Fl_003AD_0021_003FTool_003FDevelopment_0020Tools_003FJetBrains_003F_002EJetBrains_003F_002ERider_003Fconfig_003Fresharper_002Dhost_003FSourcesCache_003F59c43162311a41dc4e9a06273ab3fb541e6a64318777591f77584020ff865_003FSolutionState_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=c33c8ad6_002Dd992_002D4817_002D855f_002D764f7c7897fa/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Generates_ContextAware_Code" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD; <s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=c33c8ad6_002Dd992_002D4817_002D855f_002D764f7c7897fa/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Generates_ContextAware_Code" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD; &lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;NUnit3x::BB047F43-6AA0-4EA0-8AE9-E6B9784D9E8E::net8.0::GFramework.SourceGenerators.Tests.rule.ContextAwareGeneratorTests&lt;/TestId&gt;&#xD; &lt;TestId&gt;NUnit3x::BB047F43-6AA0-4EA0-8AE9-E6B9784D9E8E::net8.0::GFramework.SourceGenerators.Tests.rule.ContextAwareGeneratorTests&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD; &lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=cb493c10_002D1cc7_002D408b_002Db1eb_002D2e8a3ffd30ee/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Generates_ContextAware_Code" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;NUnit3x::BB047F43-6AA0-4EA0-8AE9-E6B9784D9E8E::net8.0::GFramework.SourceGenerators.Tests.rule.ContextAwareGeneratorTests.Generates_ContextAware_Code&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary> &lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>