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