From 6fa45808939164fa4009990b594a2e68d6db9f64 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:11:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=20BindNod?= =?UTF-8?q?eSignal=20=E5=92=8C=20GetNode=20=E6=BA=90=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 BindNodeSignalGenerator 用于生成节点信号绑定与解绑逻辑 - 实现 GetNodeGenerator 用于生成 Godot 节点获取注入逻辑 - 添加 BindNodeSignalDiagnostics 提供详细的诊断错误信息 - 集成到 AnalyzerReleases.Unshipped.md 追踪新的分析规则 - 支持 [BindNodeSignal] 属性的方法自动生成事件绑定代码 - 支持 [GetNode] 属性的字段自动生成节点获取代码 - 提供生命周期方法集成的智能提示和验证功能 --- .../BindNodeSignalGeneratorTests.cs | 4 +- .../GetNode/GetNodeGeneratorTests.cs | 71 ++++++++++++++++- .../AnalyzerReleases.Unshipped.md | 1 - .../BindNodeSignalGenerator.cs | 39 ++-------- .../Diagnostics/BindNodeSignalDiagnostics.cs | 12 --- .../GetNodeGenerator.cs | 12 +-- .../AnalyzerReleases.Unshipped.md | 5 +- .../Diagnostics/CommonDiagnostics.cs | 21 ++++- .../GeneratedMethodConflictExtensions.cs | 49 ++++++++++++ .../Rule/ContextGetGeneratorTests.cs | 78 +++++++++++++++++++ .../Rule/ContextGetGenerator.cs | 8 +- 11 files changed, 236 insertions(+), 64 deletions(-) create mode 100644 GFramework.SourceGenerators.Common/Extensions/GeneratedMethodConflictExtensions.cs diff --git a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs index a479c64..7f71f21 100644 --- a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs @@ -526,11 +526,11 @@ public class BindNodeSignalGeneratorTests TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error) + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) .WithLocation(0) .WithArguments("Hud", "__BindNodeSignals_Generated")); - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error) + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) .WithLocation(1) .WithArguments("Hud", "__UnbindNodeSignals_Generated")); diff --git a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs index b0df01f..266ff98 100644 --- a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs @@ -1,8 +1,4 @@ using GFramework.Godot.SourceGenerators.Tests.Core; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; namespace GFramework.Godot.SourceGenerators.Tests.GetNode; @@ -240,4 +236,71 @@ public class GetNodeGeneratorTests await test.RunAsync(); } + + [Test] + public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public GetNodeAttribute() {} + } + + public enum NodeLookupMode + { + Auto = 0 + } + } + + namespace Godot + { + public class Node + { + public virtual void _Ready() {} + public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); + public T? GetNodeOrNull(string path) where T : Node => default; + } + + public class HBoxContainer : Node + { + } + } + + namespace TestApp + { + public partial class TopBar : HBoxContainer + { + [GetNode] + private HBoxContainer _leftContainer = null!; + + private void {|#0:__InjectGetNodes_Generated|}() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("TopBar", "__InjectGetNodes_Generated")); + + await test.RunAsync(); + } } \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md index 76b9b7a..c4a2930 100644 --- a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -21,4 +21,3 @@ GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics - GF_Godot_BindNodeSignal_011 | GFramework.Godot | Error | BindNodeSignalDiagnostics diff --git a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs index 4fa0321..d768432 100644 --- a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs +++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using GFramework.Godot.SourceGenerators.Diagnostics; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; @@ -88,7 +89,11 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (!CanGenerateForType(context, group, typeSymbol)) continue; - if (HasGeneratedMethodNameConflict(context, group, typeSymbol)) + if (typeSymbol.ReportGeneratedMethodConflicts( + context, + group.Methods[0].Method.Identifier.GetLocation(), + BindMethodName, + UnbindMethodName)) continue; var bindings = new List(); @@ -390,36 +395,6 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator typeSymbol.Name)); } - private static bool HasGeneratedMethodNameConflict( - SourceProductionContext context, - TypeGroup group, - INamedTypeSymbol typeSymbol) - { - var hasConflict = false; - - foreach (var generatedMethodName in new[] { BindMethodName, UnbindMethodName }) - { - var conflictingMethod = typeSymbol.GetMembers() - .OfType() - .FirstOrDefault(method => - method.Name == generatedMethodName && - method.Parameters.Length == 0 && - method.TypeParameters.Length == 0); - - if (conflictingMethod is null) - continue; - - context.ReportDiagnostic(Diagnostic.Create( - BindNodeSignalDiagnostics.GeneratedMethodNameConflict, - conflictingMethod.Locations.FirstOrDefault() ?? group.Methods[0].Method.Identifier.GetLocation(), - typeSymbol.Name, - generatedMethodName)); - hasConflict = true; - } - - return hasConflict; - } - private static IMethodSymbol? FindLifecycleMethod( INamedTypeSymbol typeSymbol, string methodName) @@ -627,7 +602,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator public int GetHashCode(MethodCandidate obj) { - return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + return RuntimeHelpers.GetHashCode(obj); } } } \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs index 41bac93..95f4b2d 100644 --- a/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs +++ b/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs @@ -126,16 +126,4 @@ public static class BindNodeSignalDiagnostics PathContests.GodotNamespace, DiagnosticSeverity.Error, true); - - /// - /// 用户代码中已存在与生成方法同名的成员。 - /// - public static readonly DiagnosticDescriptor GeneratedMethodNameConflict = - new( - "GF_Godot_BindNodeSignal_011", - "Generated method name conflicts with an existing member", - "Class '{0}' already defines method '{1}()', which conflicts with [BindNodeSignal] generated code", - PathContests.GodotNamespace, - DiagnosticSeverity.Error, - true); } \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs index 1ac7e80..280171d 100644 --- a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs +++ b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs @@ -1,12 +1,6 @@ -using System.Collections.Immutable; -using System.Text; using GFramework.Godot.SourceGenerators.Diagnostics; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; -using GFramework.SourceGenerators.Common.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace GFramework.Godot.SourceGenerators; @@ -95,6 +89,12 @@ public sealed class GetNodeGenerator : IIncrementalGenerator if (!CanGenerateForType(context, group, typeSymbol)) continue; + if (typeSymbol.ReportGeneratedMethodConflicts( + context, + group.Fields[0].Variable.Identifier.GetLocation(), + InjectionMethodName)) + continue; + var bindings = new List(); foreach (var candidate in group.Fields) diff --git a/GFramework.SourceGenerators.Common/AnalyzerReleases.Unshipped.md b/GFramework.SourceGenerators.Common/AnalyzerReleases.Unshipped.md index 84445da..35c6296 100644 --- a/GFramework.SourceGenerators.Common/AnalyzerReleases.Unshipped.md +++ b/GFramework.SourceGenerators.Common/AnalyzerReleases.Unshipped.md @@ -1,9 +1,10 @@ ; Unshipped analyzer release -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md ### New Rules Rule ID | Category | Severity | Notes ---------------------|-------------------|----------|------------------- GF_Common_Class_001 | GFramework.Common | Error | CommonDiagnostics - GF_Common_Trace_001 | GFramework.Trace | Info | CommonDiagnostics \ No newline at end of file + GF_Common_Class_002 | GFramework.Common | Error | CommonDiagnostics + GF_Common_Trace_001 | GFramework.Trace | Info | CommonDiagnostics diff --git a/GFramework.SourceGenerators.Common/Diagnostics/CommonDiagnostics.cs b/GFramework.SourceGenerators.Common/Diagnostics/CommonDiagnostics.cs index 87cb06c..c48b40e 100644 --- a/GFramework.SourceGenerators.Common/Diagnostics/CommonDiagnostics.cs +++ b/GFramework.SourceGenerators.Common/Diagnostics/CommonDiagnostics.cs @@ -1,6 +1,4 @@ -using Microsoft.CodeAnalysis; - -namespace GFramework.SourceGenerators.Common.Diagnostics; +namespace GFramework.SourceGenerators.Common.Diagnostics; /// /// 提供通用诊断描述符的静态类 @@ -27,6 +25,23 @@ public static class CommonDiagnostics true ); + /// + /// 定义生成方法名与用户代码冲突的诊断描述符。 + /// + /// + /// 该诊断用于保护生成器保留的方法名,避免用户代码手动声明了相同零参数方法时出现重复成员错误, + /// 并使多个生成器可以复用同一条一致的冲突报告规则。 + /// + public static readonly DiagnosticDescriptor GeneratedMethodNameConflict = + new( + "GF_Common_Class_002", + "Generated method name conflicts with an existing member", + "Class '{0}' already defines method '{1}()', which conflicts with generated code", + "GFramework.Common", + DiagnosticSeverity.Error, + true + ); + /// /// 定义源代码生成器跟踪信息的诊断描述符 /// diff --git a/GFramework.SourceGenerators.Common/Extensions/GeneratedMethodConflictExtensions.cs b/GFramework.SourceGenerators.Common/Extensions/GeneratedMethodConflictExtensions.cs new file mode 100644 index 0000000..bdd1ad9 --- /dev/null +++ b/GFramework.SourceGenerators.Common/Extensions/GeneratedMethodConflictExtensions.cs @@ -0,0 +1,49 @@ +using GFramework.SourceGenerators.Common.Diagnostics; + +namespace GFramework.SourceGenerators.Common.Extensions; + +/// +/// 提供生成方法名冲突校验的通用扩展。 +/// +public static class GeneratedMethodConflictExtensions +{ + /// + /// 检查目标类型上是否已存在与生成器保留方法同名的零参数方法,并在冲突时报告统一诊断。 + /// + /// 待校验的目标类型。 + /// 源代码生成上下文。 + /// 当冲突成员缺少源码位置时使用的后备位置。 + /// 生成器将保留的零参数方法名集合。 + /// 若发现任一冲突则返回 true + public static bool ReportGeneratedMethodConflicts( + this INamedTypeSymbol typeSymbol, + SourceProductionContext context, + Location fallbackLocation, + params string[] generatedMethodNames) + { + var hasConflict = false; + + foreach (var generatedMethodName in generatedMethodNames.Distinct(StringComparer.Ordinal)) + { + var conflictingMethod = typeSymbol.GetMembers() + .OfType() + .FirstOrDefault(method => + !method.IsImplicitlyDeclared && + string.Equals(method.Name, generatedMethodName, StringComparison.Ordinal) && + method.Parameters.Length == 0 && + method.TypeParameters.Length == 0); + + if (conflictingMethod is null) + continue; + + context.ReportDiagnostic(Diagnostic.Create( + CommonDiagnostics.GeneratedMethodNameConflict, + conflictingMethod.Locations.FirstOrDefault() ?? fallbackLocation, + typeSymbol.Name, + generatedMethodName)); + hasConflict = true; + } + + return hasConflict; + } +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs index 1bea458..c8fec0f 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs @@ -377,6 +377,84 @@ public class ContextGetGeneratorTests Assert.Pass(); } + [Test] + public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() + { + var source = """ + using System; + using GFramework.SourceGenerators.Abstractions.Rule; + + namespace GFramework.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class ContextAwareAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field, Inherited = false)] + public sealed class GetModelAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware { } + } + + namespace GFramework.Core.Abstractions.Model + { + public interface IModel { } + } + + namespace GFramework.Core.Abstractions.Systems + { + public interface ISystem { } + } + + namespace GFramework.Core.Abstractions.Utility + { + public interface IUtility { } + } + + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + } + } + + namespace TestApp + { + public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } + + [ContextAware] + public partial class InventoryPanel + { + [GetModel] + private IInventoryModel _model = null!; + + private void {|#0:__InjectContextBindings_Generated|}() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("InventoryPanel", "__InjectContextBindings_Generated")); + + await test.RunAsync(); + } + [Test] public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic() { diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs index 1263d11..d6bae00 100644 --- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -1,7 +1,5 @@ -using System.Text; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; -using GFramework.SourceGenerators.Common.Extensions; using GFramework.SourceGenerators.Common.Info; using GFramework.SourceGenerators.Diagnostics; @@ -220,6 +218,12 @@ public sealed class ContextGetGenerator : IIncrementalGenerator if (!CanGenerateForType(context, workItem, symbols)) continue; + if (workItem.TypeSymbol.ReportGeneratedMethodConflicts( + context, + GetTypeLocation(workItem), + InjectionMethodName)) + continue; + var bindings = CollectBindings(context, workItem, descriptors, symbols); if (bindings.Count == 0 && workItem.GetAllDeclaration is null) continue;