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 = """
namespace GFramework.Core.rule
{
public interface IContextAware
{
void SetContext(GFramework.Core.architecture.IArchitectureContext context);
}
}
namespace GFramework.Core.architecture
{
public interface IArchitectureContext {}
@ -61,7 +53,7 @@ public class ContextAwareGeneratorTests
#nullable enable
namespace TestApp;
partial class MyRule : GFramework.Core.rule.IContextAware
partial class MyRule
{
protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!;
void GFramework.Core.rule.IContextAware.SetContext(

View File

@ -4,5 +4,6 @@
### New Rules
Rule ID | Category | Severity | Notes
----------------|--------------------------|----------|-------------------
----------------|----------------------------------|----------|------------------------
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",
"LogAttribute cannot generate Logger",
"LogAttribute on class '{0}' is ineffective: {1}",
"GFramework.Godot.Logging",
"GFramework.Godot.logging",
DiagnosticSeverity.Warning,
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)
{
// 1. 找到所有 class 声明
var classDeclarations = context.SyntaxProvider
// 1️⃣ 查找候选类
var candidates = 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!)
);
// 2⃣ 注册生成输出
context.RegisterSourceOutput(candidates, static (spc, symbol) =>
{
if (symbol != null)
GenerateOutput(spc, symbol);
});
}
#region
private static INamedTypeSymbol? GetCandidate(GeneratorSyntaxContext context)
{
var classDecl = (ClassDeclarationSyntax)context.Node;
if (classDecl.AttributeLists.Count == 0)
if (context.SemanticModel.GetDeclaredSymbol(context.Node) is not INamedTypeSymbol symbol)
return null;
if (context.SemanticModel.GetDeclaredSymbol(classDecl)
is not { } symbol)
return null;
// 仅筛选带有 ContextAwareAttribute 的类
var hasAttr = symbol.GetAttributes()
.Any(attr => attr.AttributeClass?.ToDisplayString() == AttributeMetadataName);
return Enumerable.Any(symbol.GetAttributes(),
attr => attr.AttributeClass?.ToDisplayString() == AttributeMetadataName)
? symbol
: null;
return hasAttr ? symbol : null;
}
#endregion
private static void Generate(
SourceProductionContext context,
INamedTypeSymbol symbol)
{
var syntax = symbol.DeclaringSyntaxReferences
.FirstOrDefault()?
.GetSyntax() as ClassDeclarationSyntax;
#region +
if (syntax is null || !syntax.Modifiers.Any(SyntaxKind.PartialKeyword))
private static void GenerateOutput(SourceProductionContext context, INamedTypeSymbol symbol)
{
context.ReportDiagnostic(
Diagnostic.Create(
var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as ClassDeclarationSyntax;
if (syntax == null)
return;
// 1⃣ 必须是 partial
if (!syntax.Modifiers.Any(SyntaxKind.PartialKeyword))
{
context.ReportDiagnostic(Diagnostic.Create(
CommonDiagnostics.ClassMustBePartial,
syntax?.Identifier.GetLocation(),
syntax.Identifier.GetLocation(),
symbol.Name
)
);
));
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
? null
: symbol.ContainingNamespace.ToDisplayString();
var source = GenerateSource(ns, symbol);
context.AddSource(
context
.AddSource(
$"{symbol.Name}.ContextAware.g.cs",
source
);
source);
}
#endregion
#region
private static string GenerateSource(string? ns, INamedTypeSymbol symbol)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable");
@ -93,20 +105,20 @@ public sealed class ContextAwareGenerator : IIncrementalGenerator
sb.AppendLine();
}
sb.AppendLine($"partial class {symbol.Name} : GFramework.Core.rule.IContextAware");
sb.AppendLine($"partial class {symbol.Name}");
sb.AppendLine("{");
sb.AppendLine(
" protected GFramework.Core.architecture.IArchitectureContext Context { get; private set; } = null!;");
sb.AppendLine();
sb.AppendLine(" void GFramework.Core.rule.IContextAware.SetContext(");
sb.AppendLine(" GFramework.Core.architecture.IArchitectureContext context)");
sb.AppendLine(" {");
sb.AppendLine(" Context = context;");
sb.AppendLine(" }");
sb.AppendLine("}");
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">
<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_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_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/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;TestId&gt;NUnit3x::BB047F43-6AA0-4EA0-8AE9-E6B9784D9E8E::net8.0::GFramework.SourceGenerators.Tests.rule.ContextAwareGeneratorTests&lt;/TestId&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>