mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-01 02:56:44 +08:00
feat(Godot.SourceGenerators): 添加 BindNodeSignal 源代码生成器
- 实现 BindNodeSignalGenerator 源代码生成器,用于自动化节点信号绑定 - 添加完整的诊断系统,包含 11 种不同的错误和警告场景检测 - 生成对称的绑定和解绑方法,确保资源正确释放 - 支持一个处理方法通过多个特性绑定到多个节点事件 - 实现生命周期钩子调用检查,确保在 _Ready 和 _ExitTree 中正确调用生成的方法 - 提供详细的单元测试覆盖各种使用场景和边界条件 - 生成器与现有的 GetNode 声明完全兼容并可共存 - 包含命名冲突检测和构造参数验证等安全检查机制
This commit is contained in:
parent
9cca190aff
commit
2dfd6e044f
@ -375,6 +375,168 @@ public class BindNodeSignalGeneratorTests
|
|||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
|
||||||
|
/// </summary>
|
||||||
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
|
||||||
|
/// </summary>
|
||||||
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
|
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -20,3 +20,5 @@
|
|||||||
GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_009 | 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
|
||||||
|
|||||||
@ -40,7 +40,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
return methodDeclaration.AttributeLists
|
return methodDeclaration.AttributeLists
|
||||||
.SelectMany(static list => list.Attributes)
|
.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)
|
private static MethodCandidate? Transform(GeneratorSyntaxContext context)
|
||||||
@ -68,10 +68,18 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
|
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var methodCandidates = candidates
|
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。
|
||||||
|
var methodAttributes = candidates
|
||||||
.Where(static candidate => candidate is not null)
|
.Where(static candidate => candidate is not null)
|
||||||
.Select(static candidate => candidate!)
|
.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();
|
.ToList();
|
||||||
|
|
||||||
foreach (var group in GroupByContainingType(methodCandidates))
|
foreach (var group in GroupByContainingType(methodCandidates))
|
||||||
@ -80,11 +88,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
if (!CanGenerateForType(context, group, typeSymbol))
|
if (!CanGenerateForType(context, group, typeSymbol))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (HasGeneratedMethodNameConflict(context, group, typeSymbol))
|
||||||
|
continue;
|
||||||
|
|
||||||
var bindings = new List<SignalBindingInfo>();
|
var bindings = new List<SignalBindingInfo>();
|
||||||
|
|
||||||
foreach (var candidate in group.Methods)
|
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))
|
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
|
||||||
continue;
|
continue;
|
||||||
@ -161,8 +172,29 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeFieldName = ResolveCtorString(attribute, 0);
|
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName))
|
||||||
var signalName = ResolveCtorString(attribute, 1);
|
{
|
||||||
|
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);
|
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
|
||||||
if (fieldSymbol is null)
|
if (fieldSymbol is null)
|
||||||
@ -244,14 +276,25 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs));
|
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveCtorString(
|
private static bool TryResolveCtorString(
|
||||||
AttributeData attribute,
|
AttributeData attribute,
|
||||||
int index)
|
int index,
|
||||||
|
out string value)
|
||||||
{
|
{
|
||||||
if (attribute.ConstructorArguments.Length <= index)
|
value = string.Empty;
|
||||||
return 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<AttributeData> ResolveAttributes(
|
private static IReadOnlyList<AttributeData> ResolveAttributes(
|
||||||
@ -347,6 +390,36 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
typeSymbol.Name));
|
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<IMethodSymbol>()
|
||||||
|
.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(
|
private static IMethodSymbol? FindLifecycleMethod(
|
||||||
INamedTypeSymbol typeSymbol,
|
INamedTypeSymbol typeSymbol,
|
||||||
string methodName)
|
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(
|
private static string GenerateSource(
|
||||||
INamedTypeSymbol typeSymbol,
|
INamedTypeSymbol typeSymbol,
|
||||||
IReadOnlyList<SignalBindingInfo> bindings)
|
IReadOnlyList<SignalBindingInfo> bindings)
|
||||||
@ -520,4 +610,24 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
public List<MethodCandidate> Methods { get; }
|
public List<MethodCandidate> Methods { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用引用相等比较 MethodCandidate,确保缓存字典复用同一语法候选对象。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ReferenceEqualityComparer : IEqualityComparer<MethodCandidate>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -114,4 +114,28 @@ public static class BindNodeSignalDiagnostics
|
|||||||
PathContests.GodotNamespace,
|
PathContests.GodotNamespace,
|
||||||
DiagnosticSeverity.Warning,
|
DiagnosticSeverity.Warning,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BindNodeSignalAttribute 构造参数无效。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户代码中已存在与生成方法同名的成员。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user