From 5b996d86180cd40f704f20a23c99e91ff29c6f71 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:39:06 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=20Bin?= =?UTF-8?q?dNodeSignal=20=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 BindNodeSignalGenerator 源生成器,用于自动生成 Godot 节点事件绑定与解绑逻辑 - 添加 BindNodeSignalAttribute 特性,标记需要生成绑定逻辑的事件处理方法 - 实现完整的诊断系统,包括嵌套类型、静态方法、字段类型等错误检查 - 添加生命周期方法调用检查,在 _Ready 和 _ExitTree 中验证生成方法的调用 - 支持方法签名与事件委托的兼容性验证 - 实现单元测试覆盖各种使用场景和错误情况 --- .../BindNodeSignalAttribute.cs | 40 ++ .../BindNodeSignalGeneratorTests.cs | 467 ++++++++++++++++ .../BindNodeSignalGenerator.cs | 523 ++++++++++++++++++ .../Diagnostics/BindNodeSignalDiagnostics.cs | 117 ++++ 4 files changed, 1147 insertions(+) create mode 100644 GFramework.Godot.SourceGenerators.Abstractions/BindNodeSignalAttribute.cs create mode 100644 GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs create mode 100644 GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs create mode 100644 GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs diff --git a/GFramework.Godot.SourceGenerators.Abstractions/BindNodeSignalAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/BindNodeSignalAttribute.cs new file mode 100644 index 0000000..b7db30a --- /dev/null +++ b/GFramework.Godot.SourceGenerators.Abstractions/BindNodeSignalAttribute.cs @@ -0,0 +1,40 @@ +#nullable enable +namespace GFramework.Godot.SourceGenerators.Abstractions; + +/// +/// 标记 Godot 节点事件处理方法,Source Generator 会为其生成事件绑定与解绑逻辑。 +/// +/// +/// 该特性通过节点字段名与事件名建立声明式订阅关系,适用于将 +/// _Ready() / _ExitTree() 中重复的 +=-= 样板代码 +/// 收敛到生成器中统一维护。 +/// +[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +public sealed class BindNodeSignalAttribute : Attribute +{ + /// + /// 初始化 的新实例。 + /// + /// 目标节点字段名。 + /// 目标节点上的 CLR 事件名。 + /// + /// 。 + /// + public BindNodeSignalAttribute( + string nodeFieldName, + string signalName) + { + NodeFieldName = nodeFieldName ?? throw new ArgumentNullException(nameof(nodeFieldName)); + SignalName = signalName ?? throw new ArgumentNullException(nameof(signalName)); + } + + /// + /// 获取目标节点字段名。 + /// + public string NodeFieldName { get; } + + /// + /// 获取目标节点上的 CLR 事件名。 + /// + public string SignalName { get; } +} \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs new file mode 100644 index 0000000..8f07a84 --- /dev/null +++ b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs @@ -0,0 +1,467 @@ +using GFramework.Godot.SourceGenerators.Tests.Core; + +namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal; + +/// +/// 验证 的生成与诊断行为。 +/// +[TestFixture] +public class BindNodeSignalGeneratorTests +{ + /// + /// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。 + /// + [Test] + public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + } + + namespace Godot + { + public class Node + { + public virtual void _Ready() {} + + public virtual void _ExitTree() {} + } + + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + + public class SpinBox : Node + { + public delegate void ValueChangedEventHandler(double value); + + public event ValueChangedEventHandler? ValueChanged + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + private Button _startButton = null!; + private SpinBox _startOreSpinBox = null!; + + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } + + [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] + private void OnStartOreValueChanged(double value) + { + } + + public override void _Ready() + { + __BindNodeSignals_Generated(); + } + + public override void _ExitTree() + { + __UnbindNodeSignals_Generated(); + } + } + } + """; + + const string expected = """ + // + #nullable enable + + namespace TestApp; + + partial class Hud + { + private void __BindNodeSignals_Generated() + { + _startButton.Pressed += OnStartButtonPressed; + _startOreSpinBox.ValueChanged += OnStartOreValueChanged; + } + + private void __UnbindNodeSignals_Generated() + { + _startButton.Pressed -= OnStartButtonPressed; + _startOreSpinBox.ValueChanged -= OnStartOreValueChanged; + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_Hud.BindNodeSignal.g.cs", expected)); + } + + /// + /// 验证一个处理方法可以通过多个特性绑定到多个节点事件,且能与 GetNode 声明共存。 + /// + [Test] + public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + } + } + + namespace Godot + { + public class Node + { + public virtual void _Ready() {} + + public virtual void _ExitTree() {} + } + + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + [GetNode] + private Button _startButton = null!; + + [GetNode] + private Button _cancelButton = null!; + + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))] + private void OnAnyButtonPressed() + { + } + } + } + """; + + const string expected = """ + // + #nullable enable + + namespace TestApp; + + partial class Hud + { + private void __BindNodeSignals_Generated() + { + _startButton.Pressed += OnAnyButtonPressed; + _cancelButton.Pressed += OnAnyButtonPressed; + } + + private void __UnbindNodeSignals_Generated() + { + _startButton.Pressed -= OnAnyButtonPressed; + _cancelButton.Pressed -= OnAnyButtonPressed; + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_Hud.BindNodeSignal.g.cs", expected)); + } + + /// + /// 验证引用不存在的事件时会报告错误。 + /// + [Test] + public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + } + + namespace Godot + { + public class Node + { + } + + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + private Button _startButton = null!; + + [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] + private void OnStartButtonPressed() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("_startButton", "Released")); + + await test.RunAsync(); + } + + /// + /// 验证方法签名与事件委托不匹配时会报告错误。 + /// + [Test] + public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + } + + namespace Godot + { + public class Node + { + } + + public class SpinBox : Node + { + public delegate void ValueChangedEventHandler(double value); + + public event ValueChangedEventHandler? ValueChanged + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + private SpinBox _startOreSpinBox = null!; + + [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] + private void OnStartOreValueChanged() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")); + + await test.RunAsync(); + } + + /// + /// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。 + /// + [Test] + public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + } + + namespace Godot + { + public class Node + { + public virtual void _Ready() {} + + public virtual void _ExitTree() {} + } + + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + private Button _startButton = null!; + + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } + + public override void {|#0:_Ready|}() + { + } + + public override void {|#1:_ExitTree|}() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) + .WithLocation(0) + .WithArguments("Hud")); + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) + .WithLocation(1) + .WithArguments("Hud")); + + await test.RunAsync(); + } +} \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs new file mode 100644 index 0000000..27d8cb7 --- /dev/null +++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs @@ -0,0 +1,523 @@ +using GFramework.Godot.SourceGenerators.Diagnostics; +using GFramework.SourceGenerators.Common.Constants; +using GFramework.SourceGenerators.Common.Diagnostics; + +namespace GFramework.Godot.SourceGenerators; + +/// +/// 为带有 [BindNodeSignal] 的方法生成 Godot 节点事件绑定与解绑逻辑。 +/// +[Generator] +public sealed class BindNodeSignalGenerator : IIncrementalGenerator +{ + private const string BindNodeSignalAttributeMetadataName = + $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.BindNodeSignalAttribute"; + + private const string BindMethodName = "__BindNodeSignals_Generated"; + private const string UnbindMethodName = "__UnbindNodeSignals_Generated"; + + /// + /// 初始化增量生成器。 + /// + /// 生成器初始化上下文。 + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var candidates = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => IsCandidate(node), + static (ctx, _) => Transform(ctx)) + .Where(static candidate => candidate is not null); + + var compilationAndCandidates = context.CompilationProvider.Combine(candidates.Collect()); + + context.RegisterSourceOutput(compilationAndCandidates, + static (spc, pair) => Execute(spc, pair.Left, pair.Right)); + } + + private static bool IsCandidate(SyntaxNode node) + { + if (node is not MethodDeclarationSyntax methodDeclaration) + return false; + + return methodDeclaration.AttributeLists + .SelectMany(static list => list.Attributes) + .Any(static attribute => attribute.Name.ToString().Contains("BindNodeSignal", StringComparison.Ordinal)); + } + + private static MethodCandidate? Transform(GeneratorSyntaxContext context) + { + if (context.Node is not MethodDeclarationSyntax methodDeclaration) + return null; + + if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is not IMethodSymbol methodSymbol) + return null; + + return new MethodCandidate(methodDeclaration, methodSymbol); + } + + private static void Execute( + SourceProductionContext context, + Compilation compilation, + ImmutableArray candidates) + { + if (candidates.IsDefaultOrEmpty) + return; + + var bindNodeSignalAttribute = compilation.GetTypeByMetadataName(BindNodeSignalAttributeMetadataName); + var godotNodeSymbol = compilation.GetTypeByMetadataName("Godot.Node"); + + if (bindNodeSignalAttribute is null || godotNodeSymbol is null) + return; + + var methodCandidates = candidates + .Where(static candidate => candidate is not null) + .Select(static candidate => candidate!) + .Where(candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute).Count > 0) + .ToList(); + + foreach (var group in GroupByContainingType(methodCandidates)) + { + var typeSymbol = group.TypeSymbol; + if (!CanGenerateForType(context, group, typeSymbol)) + continue; + + var bindings = new List(); + + foreach (var candidate in group.Methods) + { + foreach (var attribute in ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute)) + { + if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding)) + continue; + + bindings.Add(binding); + } + } + + if (bindings.Count == 0) + continue; + + ReportMissingLifecycleHookCall( + context, + group, + typeSymbol, + "_Ready", + BindMethodName, + BindNodeSignalDiagnostics.ManualReadyHookRequired); + + ReportMissingLifecycleHookCall( + context, + group, + typeSymbol, + "_ExitTree", + UnbindMethodName, + BindNodeSignalDiagnostics.ManualExitTreeHookRequired); + + context.AddSource(GetHintName(typeSymbol), GenerateSource(typeSymbol, bindings)); + } + } + + private static bool CanGenerateForType( + SourceProductionContext context, + TypeGroup group, + INamedTypeSymbol typeSymbol) + { + if (typeSymbol.ContainingType is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + BindNodeSignalDiagnostics.NestedClassNotSupported, + group.Methods[0].Method.Identifier.GetLocation(), + typeSymbol.Name)); + return false; + } + + if (typeSymbol.AreAllDeclarationsPartial()) + return true; + + context.ReportDiagnostic(Diagnostic.Create( + CommonDiagnostics.ClassMustBePartial, + group.Methods[0].Method.Identifier.GetLocation(), + typeSymbol.Name)); + + return false; + } + + private static bool TryCreateBinding( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + INamedTypeSymbol godotNodeSymbol, + out SignalBindingInfo binding) + { + binding = default!; + + if (candidate.MethodSymbol.IsStatic) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.StaticMethodNotSupported, + candidate, + attribute, + candidate.MethodSymbol.Name); + return false; + } + + var nodeFieldName = ResolveCtorString(attribute, 0); + var signalName = ResolveCtorString(attribute, 1); + + var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); + if (fieldSymbol is null) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.NodeFieldNotFound, + candidate, + attribute, + candidate.MethodSymbol.Name, + nodeFieldName, + candidate.MethodSymbol.ContainingType.Name); + return false; + } + + if (fieldSymbol.IsStatic) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField, + candidate, + attribute, + candidate.MethodSymbol.Name, + fieldSymbol.Name); + return false; + } + + if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol)) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode, + candidate, + attribute, + fieldSymbol.Name); + return false; + } + + var eventSymbol = FindEvent(fieldSymbol.Type, signalName); + if (eventSymbol is null) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.SignalNotFound, + candidate, + attribute, + fieldSymbol.Name, + signalName); + return false; + } + + if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol)) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.MethodSignatureNotCompatible, + candidate, + attribute, + candidate.MethodSymbol.Name, + eventSymbol.Name, + fieldSymbol.Name); + return false; + } + + binding = new SignalBindingInfo(fieldSymbol, eventSymbol, candidate.MethodSymbol); + return true; + } + + private static void ReportMethodDiagnostic( + SourceProductionContext context, + DiagnosticDescriptor descriptor, + MethodCandidate candidate, + AttributeData attribute, + params object[] messageArgs) + { + var location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? + candidate.Method.Identifier.GetLocation(); + + context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs)); + } + + private static string ResolveCtorString( + AttributeData attribute, + int index) + { + if (attribute.ConstructorArguments.Length <= index) + return string.Empty; + + return attribute.ConstructorArguments[index].Value as string ?? string.Empty; + } + + private static IReadOnlyList ResolveAttributes( + IMethodSymbol methodSymbol, + INamedTypeSymbol bindNodeSignalAttribute) + { + return methodSymbol.GetAttributes() + .Where(attribute => + SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, bindNodeSignalAttribute)) + .ToList(); + } + + private static IFieldSymbol? FindField( + INamedTypeSymbol typeSymbol, + string nodeFieldName) + { + return typeSymbol.GetMembers() + .OfType() + .FirstOrDefault(field => string.Equals(field.Name, nodeFieldName, StringComparison.Ordinal)); + } + + private static IEventSymbol? FindEvent( + ITypeSymbol typeSymbol, + string signalName) + { + for (var current = typeSymbol as INamedTypeSymbol; current is not null; current = current.BaseType) + { + var eventSymbol = current.GetMembers() + .OfType() + .FirstOrDefault(evt => string.Equals(evt.Name, signalName, StringComparison.Ordinal)); + + if (eventSymbol is not null) + return eventSymbol; + } + + return null; + } + + private static bool IsMethodCompatibleWithEvent( + IMethodSymbol methodSymbol, + IEventSymbol eventSymbol) + { + if (!methodSymbol.ReturnsVoid) + return false; + + if (methodSymbol.TypeParameters.Length > 0) + return false; + + if (eventSymbol.Type is not INamedTypeSymbol delegateType) + return false; + + var invokeMethod = delegateType.DelegateInvokeMethod; + if (invokeMethod is null || !invokeMethod.ReturnsVoid) + return false; + + if (methodSymbol.Parameters.Length != invokeMethod.Parameters.Length) + return false; + + // 这里采用“精确签名匹配”而不是宽松推断,确保生成代码的订阅行为可预测且诊断明确。 + for (var index = 0; index < methodSymbol.Parameters.Length; index++) + { + var methodParameter = methodSymbol.Parameters[index]; + var delegateParameter = invokeMethod.Parameters[index]; + + if (methodParameter.RefKind != delegateParameter.RefKind) + return false; + + var methodParameterType = methodParameter.Type.WithNullableAnnotation(NullableAnnotation.None); + var delegateParameterType = delegateParameter.Type.WithNullableAnnotation(NullableAnnotation.None); + + if (!SymbolEqualityComparer.Default.Equals(methodParameterType, delegateParameterType)) + return false; + } + + return true; + } + + private static void ReportMissingLifecycleHookCall( + SourceProductionContext context, + TypeGroup group, + INamedTypeSymbol typeSymbol, + string lifecycleMethodName, + string generatedMethodName, + DiagnosticDescriptor descriptor) + { + var lifecycleMethod = FindLifecycleMethod(typeSymbol, lifecycleMethodName); + if (lifecycleMethod is null || CallsGeneratedMethod(lifecycleMethod, generatedMethodName)) + return; + + context.ReportDiagnostic(Diagnostic.Create( + descriptor, + lifecycleMethod.Locations.FirstOrDefault() ?? group.Methods[0].Method.Identifier.GetLocation(), + typeSymbol.Name)); + } + + private static IMethodSymbol? FindLifecycleMethod( + INamedTypeSymbol typeSymbol, + string methodName) + { + return typeSymbol.GetMembers() + .OfType() + .FirstOrDefault(method => + method.Name == methodName && + !method.IsStatic && + method.Parameters.Length == 0 && + method.MethodKind == MethodKind.Ordinary); + } + + private static bool CallsGeneratedMethod( + IMethodSymbol methodSymbol, + string generatedMethodName) + { + foreach (var syntaxReference in methodSymbol.DeclaringSyntaxReferences) + { + if (syntaxReference.GetSyntax() is not MethodDeclarationSyntax methodSyntax) + continue; + + if (methodSyntax.DescendantNodes() + .OfType() + .Any(invocation => IsGeneratedMethodInvocation(invocation, generatedMethodName))) + return true; + } + + return false; + } + + private static bool IsGeneratedMethodInvocation( + InvocationExpressionSyntax invocation, + string generatedMethodName) + { + return invocation.Expression switch + { + IdentifierNameSyntax identifierName => string.Equals( + identifierName.Identifier.ValueText, + generatedMethodName, + StringComparison.Ordinal), + MemberAccessExpressionSyntax memberAccess => string.Equals( + memberAccess.Name.Identifier.ValueText, + generatedMethodName, + StringComparison.Ordinal), + _ => false + }; + } + + private static string GenerateSource( + INamedTypeSymbol typeSymbol, + IReadOnlyList bindings) + { + var namespaceName = typeSymbol.GetNamespace(); + var generics = typeSymbol.ResolveGenerics(); + + var sb = new StringBuilder() + .AppendLine("// ") + .AppendLine("#nullable enable"); + + if (namespaceName is not null) + { + sb.AppendLine() + .AppendLine($"namespace {namespaceName};"); + } + + sb.AppendLine() + .AppendLine($"partial class {typeSymbol.Name}{generics.Parameters}"); + + foreach (var constraint in generics.Constraints) + sb.AppendLine($" {constraint}"); + + sb.AppendLine("{") + .AppendLine($" private void {BindMethodName}()") + .AppendLine(" {"); + + foreach (var binding in bindings) + sb.AppendLine( + $" {binding.FieldSymbol.Name}.{binding.EventSymbol.Name} += {binding.MethodSymbol.Name};"); + + sb.AppendLine(" }") + .AppendLine() + .AppendLine($" private void {UnbindMethodName}()") + .AppendLine(" {"); + + foreach (var binding in bindings) + sb.AppendLine( + $" {binding.FieldSymbol.Name}.{binding.EventSymbol.Name} -= {binding.MethodSymbol.Name};"); + + sb.AppendLine(" }") + .AppendLine("}"); + + return sb.ToString(); + } + + private static string GetHintName(INamedTypeSymbol typeSymbol) + { + return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .Replace("global::", string.Empty) + .Replace("<", "_") + .Replace(">", "_") + .Replace(",", "_") + .Replace(" ", string.Empty) + .Replace(".", "_") + ".BindNodeSignal.g.cs"; + } + + private static IReadOnlyList GroupByContainingType(IEnumerable candidates) + { + var groupMap = new Dictionary(SymbolEqualityComparer.Default); + var orderedGroups = new List(); + + foreach (var candidate in candidates) + { + var typeSymbol = candidate.MethodSymbol.ContainingType; + if (!groupMap.TryGetValue(typeSymbol, out var group)) + { + group = new TypeGroup(typeSymbol); + groupMap.Add(typeSymbol, group); + orderedGroups.Add(group); + } + + group.Methods.Add(candidate); + } + + return orderedGroups; + } + + private sealed class MethodCandidate + { + public MethodCandidate( + MethodDeclarationSyntax method, + IMethodSymbol methodSymbol) + { + Method = method; + MethodSymbol = methodSymbol; + } + + public MethodDeclarationSyntax Method { get; } + + public IMethodSymbol MethodSymbol { get; } + } + + private sealed class SignalBindingInfo + { + public SignalBindingInfo( + IFieldSymbol fieldSymbol, + IEventSymbol eventSymbol, + IMethodSymbol methodSymbol) + { + FieldSymbol = fieldSymbol; + EventSymbol = eventSymbol; + MethodSymbol = methodSymbol; + } + + public IFieldSymbol FieldSymbol { get; } + + public IEventSymbol EventSymbol { get; } + + public IMethodSymbol MethodSymbol { get; } + } + + private sealed class TypeGroup + { + public TypeGroup(INamedTypeSymbol typeSymbol) + { + TypeSymbol = typeSymbol; + Methods = new List(); + } + + public INamedTypeSymbol TypeSymbol { get; } + + public List Methods { get; } + } +} \ No newline at end of file diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs new file mode 100644 index 0000000..6b48535 --- /dev/null +++ b/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs @@ -0,0 +1,117 @@ +using GFramework.SourceGenerators.Common.Constants; + +namespace GFramework.Godot.SourceGenerators.Diagnostics; + +/// +/// BindNodeSignal 生成器相关诊断。 +/// +public static class BindNodeSignalDiagnostics +{ + /// + /// 嵌套类型不受支持。 + /// + public static readonly DiagnosticDescriptor NestedClassNotSupported = + new( + "GF_Godot_BindNodeSignal_001", + "Nested classes are not supported", + "Class '{0}' cannot use [BindNodeSignal] inside a nested type", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// static 方法不受支持。 + /// + public static readonly DiagnosticDescriptor StaticMethodNotSupported = + new( + "GF_Godot_BindNodeSignal_002", + "Static methods are not supported", + "Method '{0}' cannot be static when using [BindNodeSignal]", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// 节点字段不存在。 + /// + public static readonly DiagnosticDescriptor NodeFieldNotFound = + new( + "GF_Godot_BindNodeSignal_003", + "Referenced node field was not found", + "Method '{0}' references node field '{1}', but no matching field exists on class '{2}'", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// 节点字段必须是实例字段。 + /// + public static readonly DiagnosticDescriptor NodeFieldMustBeInstanceField = + new( + "GF_Godot_BindNodeSignal_004", + "Referenced node field must be an instance field", + "Method '{0}' references node field '{1}', but that field must be an instance field", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// 字段类型必须继承自 Godot.Node。 + /// + public static readonly DiagnosticDescriptor FieldTypeMustDeriveFromNode = + new( + "GF_Godot_BindNodeSignal_005", + "Field type must derive from Godot.Node", + "Field '{0}' must be a Godot.Node type to use [BindNodeSignal]", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// 目标事件不存在。 + /// + public static readonly DiagnosticDescriptor SignalNotFound = + new( + "GF_Godot_BindNodeSignal_006", + "Referenced event was not found", + "Field '{0}' does not contain an event named '{1}'", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// 方法签名与事件委托不兼容。 + /// + public static readonly DiagnosticDescriptor MethodSignatureNotCompatible = + new( + "GF_Godot_BindNodeSignal_007", + "Method signature is not compatible with the referenced event", + "Method '{0}' is not compatible with event '{1}' on field '{2}'", + PathContests.GodotNamespace, + DiagnosticSeverity.Error, + true); + + /// + /// 现有 _Ready 中未调用生成绑定逻辑。 + /// + public static readonly DiagnosticDescriptor ManualReadyHookRequired = + new( + "GF_Godot_BindNodeSignal_008", + "Call generated signal binding from _Ready", + "Class '{0}' defines _Ready(); call __BindNodeSignals_Generated() there to bind [BindNodeSignal] handlers", + PathContests.GodotNamespace, + DiagnosticSeverity.Warning, + true); + + /// + /// 现有 _ExitTree 中未调用生成解绑逻辑。 + /// + public static readonly DiagnosticDescriptor ManualExitTreeHookRequired = + new( + "GF_Godot_BindNodeSignal_009", + "Call generated signal unbinding from _ExitTree", + "Class '{0}' defines _ExitTree(); call __UnbindNodeSignals_Generated() there to unbind [BindNodeSignal] handlers", + PathContests.GodotNamespace, + DiagnosticSeverity.Warning, + true); +} \ No newline at end of file From 9cca190aff3312d79877b0a62824d75c3a0d784f Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:03:31 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs(generator):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=92=8C=E5=88=86=E6=9E=90=E5=99=A8=E8=A7=84=E5=88=99=E6=B8=85?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AnalyzerReleases.Unshipped.md 文件记录代码分析规则 - 添加 GF_Godot_GetNode 系列规则定义(001-006) - 添加 GF_Godot_BindNodeSignal 系列规则定义(001-009) - 创建 README.md 文件详述源码生成器使用方法 - 文档化 GetNode 和 BindNodeSignal 特性用法示例 - 说明 Godot 场景相关的编译期生成能力 --- .../AnalyzerReleases.Unshipped.md | 25 +++++++---- GFramework.Godot.SourceGenerators/README.md | 45 +++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md index a78788c..b478e72 100644 --- a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -3,11 +3,20 @@ ### New Rules - Rule ID | Category | Severity | Notes -----------------------|------------------|----------|-------------------- - GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics - GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics - GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics - GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics - GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics - GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics \ No newline at end of file + Rule ID | Category | Severity | Notes +-----------------------------|------------------|----------|--------------------------- + GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics + GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics + GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics + GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics + GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics + GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics + GF_Godot_BindNodeSignal_001 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_002 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_003 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_004 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_005 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_006 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics + GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics diff --git a/GFramework.Godot.SourceGenerators/README.md b/GFramework.Godot.SourceGenerators/README.md index 291c557..87f423d 100644 --- a/GFramework.Godot.SourceGenerators/README.md +++ b/GFramework.Godot.SourceGenerators/README.md @@ -7,6 +7,7 @@ - 与 Godot 场景相关的编译期生成能力 - 基于 Roslyn 的增量生成器实现 - `[GetNode]` 字段注入,减少 `_Ready()` 里的 `GetNode()` 样板代码 +- `[BindNodeSignal]` 方法绑定,减少 `_Ready()` / `_ExitTree()` 中重复的事件订阅样板代码 ## 使用建议 @@ -43,3 +44,47 @@ public partial class TopBar : HBoxContainer - `_leftContainer` -> `%LeftContainer` - `m_rightContainer` -> `%RightContainer` + +## BindNodeSignal 用法 + +```csharp +using GFramework.Godot.SourceGenerators.Abstractions; +using Godot; + +public partial class Hud : Control +{ + [GetNode] + private Button _startButton = null!; + + [GetNode] + private SpinBox _startOreSpinBox = null!; + + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } + + [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] + private void OnStartOreValueChanged(double value) + { + } + + public override void _Ready() + { + __InjectGetNodes_Generated(); + __BindNodeSignals_Generated(); + } + + public override void _ExitTree() + { + __UnbindNodeSignals_Generated(); + } +} +``` + +生成器会产出两个辅助方法: + +- `__BindNodeSignals_Generated()`:负责统一订阅事件 +- `__UnbindNodeSignals_Generated()`:负责统一解绑事件 + +当前设计只处理 CLR event 形式的 Godot 事件绑定,不会自动调用 `Connect()` / `Disconnect()`。 From 2dfd6e044f9685dd2350b88c89938758d9567a3e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:26:44 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(Godot.SourceGenerators):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20BindNodeSignal=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 源代码生成器,用于自动化节点信号绑定 - 添加完整的诊断系统,包含 11 种不同的错误和警告场景检测 - 生成对称的绑定和解绑方法,确保资源正确释放 - 支持一个处理方法通过多个特性绑定到多个节点事件 - 实现生命周期钩子调用检查,确保在 _Ready 和 _ExitTree 中正确调用生成的方法 - 提供详细的单元测试覆盖各种使用场景和边界条件 - 生成器与现有的 GetNode 声明完全兼容并可共存 - 包含命名冲突检测和构造参数验证等安全检查机制 --- .../BindNodeSignalGeneratorTests.cs | 162 ++++++++++++++++++ .../AnalyzerReleases.Unshipped.md | 2 + .../BindNodeSignalGenerator.cs | 132 ++++++++++++-- .../Diagnostics/BindNodeSignalDiagnostics.cs | 24 +++ 4 files changed, 309 insertions(+), 11 deletions(-) diff --git a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs index 8f07a84..a479c64 100644 --- a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs @@ -375,6 +375,168 @@ public class BindNodeSignalGeneratorTests await test.RunAsync(); } + /// + /// 验证特性构造参数为空时会报告明确的参数无效诊断。 + /// + [Test] + public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + } + + namespace Godot + { + public class Node + { + } + + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + private Button _startButton = null!; + + [{|#0:BindNodeSignal(nameof(_startButton), "")|}] + private void OnStartButtonPressed() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OnStartButtonPressed", "signalName")); + + await test.RunAsync(); + } + + /// + /// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。 + /// + [Test] + public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist() + { + const string source = """ + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + } + + namespace Godot + { + public class Node + { + } + + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + } + + namespace TestApp + { + public partial class Hud : Node + { + private Button _startButton = null!; + + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } + + private void {|#0:__BindNodeSignals_Generated|}() + { + } + + private void {|#1:__UnbindNodeSignals_Generated|}() + { + } + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("Hud", "__BindNodeSignals_Generated")); + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("Hud", "__UnbindNodeSignals_Generated")); + + await test.RunAsync(); + } + /// /// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。 /// diff --git a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md index b478e72..76b9b7a 100644 --- a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -20,3 +20,5 @@ GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics 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 27d8cb7..4fa0321 100644 --- a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs +++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs @@ -40,7 +40,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator return methodDeclaration.AttributeLists .SelectMany(static list => list.Attributes) - .Any(static attribute => attribute.Name.ToString().Contains("BindNodeSignal", StringComparison.Ordinal)); + .Any(static attribute => IsBindNodeSignalAttributeName(attribute.Name)); } private static MethodCandidate? Transform(GeneratorSyntaxContext context) @@ -68,10 +68,18 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (bindNodeSignalAttribute is null || godotNodeSymbol is null) return; - var methodCandidates = candidates + // 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。 + var methodAttributes = candidates .Where(static candidate => candidate is not null) .Select(static candidate => candidate!) - .Where(candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute).Count > 0) + .ToDictionary( + static candidate => candidate, + candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute), + ReferenceEqualityComparer.Instance); + + var methodCandidates = methodAttributes + .Where(static pair => pair.Value.Count > 0) + .Select(static pair => pair.Key) .ToList(); foreach (var group in GroupByContainingType(methodCandidates)) @@ -80,11 +88,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (!CanGenerateForType(context, group, typeSymbol)) continue; + if (HasGeneratedMethodNameConflict(context, group, typeSymbol)) + continue; + var bindings = new List(); foreach (var candidate in group.Methods) { - foreach (var attribute in ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute)) + foreach (var attribute in methodAttributes[candidate]) { if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding)) continue; @@ -161,8 +172,29 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator return false; } - var nodeFieldName = ResolveCtorString(attribute, 0); - var signalName = ResolveCtorString(attribute, 1); + if (!TryResolveCtorString(attribute, 0, out var nodeFieldName)) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.InvalidConstructorArgument, + candidate, + attribute, + candidate.MethodSymbol.Name, + "nodeFieldName"); + return false; + } + + if (!TryResolveCtorString(attribute, 1, out var signalName)) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.InvalidConstructorArgument, + candidate, + attribute, + candidate.MethodSymbol.Name, + "signalName"); + return false; + } var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); if (fieldSymbol is null) @@ -244,14 +276,25 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs)); } - private static string ResolveCtorString( + private static bool TryResolveCtorString( AttributeData attribute, - int index) + int index, + out string value) { - if (attribute.ConstructorArguments.Length <= index) - return string.Empty; + value = string.Empty; - return attribute.ConstructorArguments[index].Value as string ?? string.Empty; + if (attribute.ConstructorArguments.Length <= index) + return false; + + var ctorArgument = attribute.ConstructorArguments[index]; + if (ctorArgument.Kind != TypedConstantKind.Primitive || ctorArgument.Value is not string ctorString) + return false; + + if (string.IsNullOrWhiteSpace(ctorString)) + return false; + + value = ctorString; + return true; } private static IReadOnlyList ResolveAttributes( @@ -347,6 +390,36 @@ 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) @@ -396,6 +469,23 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator }; } + private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName) + { + var simpleName = GetAttributeSimpleName(attributeName); + return simpleName is "BindNodeSignal" or "BindNodeSignalAttribute"; + } + + private static string? GetAttributeSimpleName(NameSyntax attributeName) + { + return attributeName switch + { + IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText, + QualifiedNameSyntax qualifiedName => GetAttributeSimpleName(qualifiedName.Right), + AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText, + _ => null + }; + } + private static string GenerateSource( INamedTypeSymbol typeSymbol, IReadOnlyList bindings) @@ -520,4 +610,24 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator public List Methods { get; } } + + /// + /// 使用引用相等比较 MethodCandidate,确保缓存字典复用同一语法候选对象。 + /// + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public static ReferenceEqualityComparer Instance { get; } = new(); + + public bool Equals( + MethodCandidate? x, + MethodCandidate? y) + { + return ReferenceEquals(x, y); + } + + public int GetHashCode(MethodCandidate obj) + { + return System.Runtime.CompilerServices.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 6b48535..41bac93 100644 --- a/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs +++ b/GFramework.Godot.SourceGenerators/Diagnostics/BindNodeSignalDiagnostics.cs @@ -114,4 +114,28 @@ public static class BindNodeSignalDiagnostics PathContests.GodotNamespace, DiagnosticSeverity.Warning, true); + + /// + /// BindNodeSignalAttribute 构造参数无效。 + /// + public static readonly DiagnosticDescriptor InvalidConstructorArgument = + new( + "GF_Godot_BindNodeSignal_010", + "BindNodeSignal attribute arguments are invalid", + "Method '{0}' uses [BindNodeSignal] with an invalid '{1}' constructor argument; it must be a non-empty string literal", + 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 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 4/5] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=20Bin?= =?UTF-8?q?dNodeSignal=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; From 693cad2adf90637e03b5212de0c4646ac637497c Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:09:47 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor(generators):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81=E7=9A=84=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 BindNodeSignalDiagnostics.cs 中添加 Microsoft.CodeAnalysis 引用 - 在 BindNodeSignalGenerator.cs 中添加 Roslyn 相关命名空间引用 - 在 GetNodeGenerator.cs 中添加 Roslyn 相关命名空间引用 - 在 GlobalUsings.cs 中集中管理全局命名空间引用 - 在 ContextGetGenerator.cs 中添加字符串和扩展方法引用 - 在 CommonDiagnostics.cs 中添加诊断相关命名空间引用 - 在 Common 全局引用文件中统一管理 Microsoft.CodeAnalysis 引用 --- GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs | 3 +++ GFramework.Godot.SourceGenerators/GetNodeGenerator.cs | 3 +++ GFramework.Godot.SourceGenerators/GlobalUsings.cs | 5 ++++- GFramework.SourceGenerators.Common/GlobalUsings.cs | 3 ++- GFramework.SourceGenerators/Rule/ContextGetGenerator.cs | 2 ++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs index d768432..610662a 100644 --- a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs +++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs @@ -1,7 +1,10 @@ +using System.Collections.Immutable; using System.Runtime.CompilerServices; +using System.Text; using GFramework.Godot.SourceGenerators.Diagnostics; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; +using GFramework.SourceGenerators.Common.Extensions; namespace GFramework.Godot.SourceGenerators; diff --git a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs index 280171d..6aa3ac8 100644 --- a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs +++ b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs @@ -1,6 +1,9 @@ +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; namespace GFramework.Godot.SourceGenerators; diff --git a/GFramework.Godot.SourceGenerators/GlobalUsings.cs b/GFramework.Godot.SourceGenerators/GlobalUsings.cs index 4d27181..0413980 100644 --- a/GFramework.Godot.SourceGenerators/GlobalUsings.cs +++ b/GFramework.Godot.SourceGenerators/GlobalUsings.cs @@ -15,4 +15,7 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp; +global using Microsoft.CodeAnalysis.CSharp.Syntax; \ No newline at end of file diff --git a/GFramework.SourceGenerators.Common/GlobalUsings.cs b/GFramework.SourceGenerators.Common/GlobalUsings.cs index 4d27181..cd9579a 100644 --- a/GFramework.SourceGenerators.Common/GlobalUsings.cs +++ b/GFramework.SourceGenerators.Common/GlobalUsings.cs @@ -15,4 +15,5 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Microsoft.CodeAnalysis; \ No newline at end of file diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs index d6bae00..c6261f4 100644 --- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -1,5 +1,7 @@ +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;