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