mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-14 06:34:30 +08:00
Compare commits
8 Commits
a628ade28e
...
059d5a8f41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059d5a8f41 | ||
|
|
38020c32a2 | ||
|
|
ba45171924 | ||
|
|
693cad2adf | ||
|
|
6fa4580893 | ||
|
|
2dfd6e044f | ||
|
|
9cca190aff | ||
|
|
5b996d8618 |
@ -0,0 +1,40 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记 Godot 节点事件处理方法,Source Generator 会为其生成事件绑定与解绑逻辑。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该特性通过节点字段名与事件名建立声明式订阅关系,适用于将
|
||||||
|
/// <c>_Ready()</c> / <c>_ExitTree()</c> 中重复的 <c>+=</c> 与 <c>-=</c> 样板代码
|
||||||
|
/// 收敛到生成器中统一维护。
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
|
public sealed class BindNodeSignalAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化 <see cref="BindNodeSignalAttribute" /> 的新实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeFieldName">目标节点字段名。</param>
|
||||||
|
/// <param name="signalName">目标节点上的 CLR 事件名。</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// <paramref name="nodeFieldName" /> 或 <paramref name="signalName" /> 为 <see langword="null" />。
|
||||||
|
/// </exception>
|
||||||
|
public BindNodeSignalAttribute(
|
||||||
|
string nodeFieldName,
|
||||||
|
string signalName)
|
||||||
|
{
|
||||||
|
NodeFieldName = nodeFieldName ?? throw new ArgumentNullException(nameof(nodeFieldName));
|
||||||
|
SignalName = signalName ?? throw new ArgumentNullException(nameof(signalName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目标节点字段名。
|
||||||
|
/// </summary>
|
||||||
|
public string NodeFieldName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目标节点上的 CLR 事件名。
|
||||||
|
/// </summary>
|
||||||
|
public string SignalName { get; }
|
||||||
|
}
|
||||||
@ -0,0 +1,629 @@
|
|||||||
|
using GFramework.Godot.SourceGenerators.Tests.Core;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 <see cref="BindNodeSignalGenerator" /> 的生成与诊断行为。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class BindNodeSignalGeneratorTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
|
||||||
|
/// </summary>
|
||||||
|
[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 = """
|
||||||
|
// <auto-generated />
|
||||||
|
#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<BindNodeSignalGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证一个处理方法可以通过多个特性绑定到多个节点事件,且能与 GetNode 声明共存。
|
||||||
|
/// </summary>
|
||||||
|
[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 = """
|
||||||
|
// <auto-generated />
|
||||||
|
#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<BindNodeSignalGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证引用不存在的事件时会报告错误。
|
||||||
|
/// </summary>
|
||||||
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证方法签名与事件委托不匹配时会报告错误。
|
||||||
|
/// </summary>
|
||||||
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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_Common_Class_002", DiagnosticSeverity.Error)
|
||||||
|
.WithLocation(0)
|
||||||
|
.WithArguments("Hud", "__BindNodeSignals_Generated"));
|
||||||
|
|
||||||
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||||
|
.WithLocation(1)
|
||||||
|
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
|
||||||
|
|
||||||
|
await test.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
|
||||||
|
/// </summary>
|
||||||
|
[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<BindNodeSignalGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,4 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Tests.Core;
|
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;
|
namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
|
||||||
|
|
||||||
@ -240,4 +236,71 @@ public class GetNodeGeneratorTests
|
|||||||
|
|
||||||
await test.RunAsync();
|
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<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||||
|
public T? GetNodeOrNull<T>(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<GetNodeGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -3,11 +3,21 @@
|
|||||||
|
|
||||||
### New Rules
|
### New Rules
|
||||||
|
|
||||||
Rule ID | Category | Severity | Notes
|
Rule ID | Category | Severity | Notes
|
||||||
----------------------|------------------|----------|--------------------
|
-----------------------------|------------------|----------|---------------------------
|
||||||
GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_006 | GFramework.Godot | Warning | 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
|
||||||
|
GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
|
|||||||
611
GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
Normal file
611
GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为带有 <c>[BindNodeSignal]</c> 的方法生成 Godot 节点事件绑定与解绑逻辑。
|
||||||
|
/// </summary>
|
||||||
|
[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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化增量生成器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">生成器初始化上下文。</param>
|
||||||
|
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 => IsBindNodeSignalAttributeName(attribute.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
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<MethodCandidate?> 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 methodAttributes = candidates
|
||||||
|
.Where(static candidate => candidate is not null)
|
||||||
|
.Select(static candidate => candidate!)
|
||||||
|
.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))
|
||||||
|
{
|
||||||
|
var typeSymbol = group.TypeSymbol;
|
||||||
|
if (!CanGenerateForType(context, group, typeSymbol))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (typeSymbol.ReportGeneratedMethodConflicts(
|
||||||
|
context,
|
||||||
|
group.Methods[0].Method.Identifier.GetLocation(),
|
||||||
|
BindMethodName,
|
||||||
|
UnbindMethodName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var bindings = new List<SignalBindingInfo>();
|
||||||
|
|
||||||
|
foreach (var candidate in group.Methods)
|
||||||
|
{
|
||||||
|
foreach (var attribute in methodAttributes[candidate])
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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 bool TryResolveCtorString(
|
||||||
|
AttributeData attribute,
|
||||||
|
int index,
|
||||||
|
out string value)
|
||||||
|
{
|
||||||
|
value = 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(
|
||||||
|
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<IFieldSymbol>()
|
||||||
|
.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<IEventSymbol>()
|
||||||
|
.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<IMethodSymbol>()
|
||||||
|
.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<InvocationExpressionSyntax>()
|
||||||
|
.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 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<SignalBindingInfo> bindings)
|
||||||
|
{
|
||||||
|
var namespaceName = typeSymbol.GetNamespace();
|
||||||
|
var generics = typeSymbol.ResolveGenerics();
|
||||||
|
|
||||||
|
var sb = new StringBuilder()
|
||||||
|
.AppendLine("// <auto-generated />")
|
||||||
|
.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<TypeGroup> GroupByContainingType(IEnumerable<MethodCandidate> candidates)
|
||||||
|
{
|
||||||
|
var groupMap = new Dictionary<INamedTypeSymbol, TypeGroup>(SymbolEqualityComparer.Default);
|
||||||
|
var orderedGroups = new List<TypeGroup>();
|
||||||
|
|
||||||
|
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<MethodCandidate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public INamedTypeSymbol TypeSymbol { 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 RuntimeHelpers.GetHashCode(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BindNodeSignal 生成器相关诊断。
|
||||||
|
/// </summary>
|
||||||
|
public static class BindNodeSignalDiagnostics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 嵌套类型不受支持。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// static 方法不受支持。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 节点字段不存在。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 节点字段必须是实例字段。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字段类型必须继承自 Godot.Node。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 目标事件不存在。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方法签名与事件委托不兼容。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 现有 _Ready 中未调用生成绑定逻辑。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 现有 _ExitTree 中未调用生成解绑逻辑。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
@ -4,9 +4,6 @@ using GFramework.Godot.SourceGenerators.Diagnostics;
|
|||||||
using GFramework.SourceGenerators.Common.Constants;
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
using GFramework.SourceGenerators.Common.Extensions;
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators;
|
namespace GFramework.Godot.SourceGenerators;
|
||||||
|
|
||||||
@ -95,6 +92,12 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
|||||||
if (!CanGenerateForType(context, group, typeSymbol))
|
if (!CanGenerateForType(context, group, typeSymbol))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (typeSymbol.ReportGeneratedMethodConflicts(
|
||||||
|
context,
|
||||||
|
group.Fields[0].Variable.Identifier.GetLocation(),
|
||||||
|
InjectionMethodName))
|
||||||
|
continue;
|
||||||
|
|
||||||
var bindings = new List<NodeBindingInfo>();
|
var bindings = new List<NodeBindingInfo>();
|
||||||
|
|
||||||
foreach (var candidate in group.Fields)
|
foreach (var candidate in group.Fields)
|
||||||
|
|||||||
@ -15,4 +15,7 @@ global using System;
|
|||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Threading;
|
global using System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
|
global using Microsoft.CodeAnalysis;
|
||||||
|
global using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
global using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
@ -7,6 +7,7 @@
|
|||||||
- 与 Godot 场景相关的编译期生成能力
|
- 与 Godot 场景相关的编译期生成能力
|
||||||
- 基于 Roslyn 的增量生成器实现
|
- 基于 Roslyn 的增量生成器实现
|
||||||
- `[GetNode]` 字段注入,减少 `_Ready()` 里的 `GetNode<T>()` 样板代码
|
- `[GetNode]` 字段注入,减少 `_Ready()` 里的 `GetNode<T>()` 样板代码
|
||||||
|
- `[BindNodeSignal]` 方法绑定,减少 `_Ready()` / `_ExitTree()` 中重复的事件订阅样板代码
|
||||||
|
|
||||||
## 使用建议
|
## 使用建议
|
||||||
|
|
||||||
@ -43,3 +44,47 @@ public partial class TopBar : HBoxContainer
|
|||||||
|
|
||||||
- `_leftContainer` -> `%LeftContainer`
|
- `_leftContainer` -> `%LeftContainer`
|
||||||
- `m_rightContainer` -> `%RightContainer`
|
- `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()`。
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
; Unshipped analyzer release
|
; 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
|
### New Rules
|
||||||
|
|
||||||
Rule ID | Category | Severity | Notes
|
Rule ID | Category | Severity | Notes
|
||||||
---------------------|-------------------|----------|-------------------
|
---------------------|-------------------|----------|-------------------
|
||||||
GF_Common_Class_001 | GFramework.Common | Error | CommonDiagnostics
|
GF_Common_Class_001 | GFramework.Common | Error | CommonDiagnostics
|
||||||
GF_Common_Trace_001 | GFramework.Trace | Info | CommonDiagnostics
|
GF_Common_Class_002 | GFramework.Common | Error | CommonDiagnostics
|
||||||
|
GF_Common_Trace_001 | GFramework.Trace | Info | CommonDiagnostics
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using Microsoft.CodeAnalysis;
|
namespace GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Common.Diagnostics;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提供通用诊断描述符的静态类
|
/// 提供通用诊断描述符的静态类
|
||||||
@ -27,6 +25,23 @@ public static class CommonDiagnostics
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义生成方法名与用户代码冲突的诊断描述符。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该诊断用于保护生成器保留的方法名,避免用户代码手动声明了相同零参数方法时出现重复成员错误,
|
||||||
|
/// 并使多个生成器可以复用同一条一致的冲突报告规则。
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 定义源代码生成器跟踪信息的诊断描述符
|
/// 定义源代码生成器跟踪信息的诊断描述符
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Common.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供生成方法名冲突校验的通用扩展。
|
||||||
|
/// </summary>
|
||||||
|
public static class GeneratedMethodConflictExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 检查目标类型上是否已存在与生成器保留方法同名的零参数方法,并在冲突时报告统一诊断。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeSymbol">待校验的目标类型。</param>
|
||||||
|
/// <param name="context">源代码生成上下文。</param>
|
||||||
|
/// <param name="fallbackLocation">当冲突成员缺少源码位置时使用的后备位置。</param>
|
||||||
|
/// <param name="generatedMethodNames">生成器将保留的零参数方法名集合。</param>
|
||||||
|
/// <returns>若发现任一冲突则返回 <c>true</c>。</returns>
|
||||||
|
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<IMethodSymbol>()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,4 +15,5 @@ global using System;
|
|||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Threading;
|
global using System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
|
global using Microsoft.CodeAnalysis;
|
||||||
@ -377,6 +377,84 @@ public class ContextGetGeneratorTests
|
|||||||
Assert.Pass();
|
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<T>(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<ContextGetGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
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]
|
[Test]
|
||||||
public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic()
|
public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -220,6 +220,12 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
if (!CanGenerateForType(context, workItem, symbols))
|
if (!CanGenerateForType(context, workItem, symbols))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (workItem.TypeSymbol.ReportGeneratedMethodConflicts(
|
||||||
|
context,
|
||||||
|
GetTypeLocation(workItem),
|
||||||
|
InjectionMethodName))
|
||||||
|
continue;
|
||||||
|
|
||||||
var bindings = CollectBindings(context, workItem, descriptors, symbols);
|
var bindings = CollectBindings(context, workItem, descriptors, symbols);
|
||||||
if (bindings.Count == 0 && workItem.GetAllDeclaration is null)
|
if (bindings.Count == 0 && workItem.GetAllDeclaration is null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
680
docs/zh-CN/source-generators/bind-node-signal-generator.md
Normal file
680
docs/zh-CN/source-generators/bind-node-signal-generator.md
Normal file
@ -0,0 +1,680 @@
|
|||||||
|
# BindNodeSignal 生成器
|
||||||
|
|
||||||
|
> 自动生成 Godot 节点信号绑定与解绑逻辑,消除事件订阅样板代码
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。它将 `_Ready()` 和
|
||||||
|
`_ExitTree()` 中重复的 `+=` 和 `-=` 样板代码收敛到生成器中统一维护。
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件
|
||||||
|
- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅
|
||||||
|
- **多事件绑定**:一个方法可以绑定到多个节点事件
|
||||||
|
- **类型安全检查**:编译时验证方法签名与事件委托的兼容性
|
||||||
|
- **与 GetNode 集成**:无缝配合 `[GetNode]` 特性使用
|
||||||
|
|
||||||
|
## 基础使用
|
||||||
|
|
||||||
|
### 标记事件处理方法
|
||||||
|
|
||||||
|
使用 `[BindNodeSignal]` 特性标记处理节点事件的方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class MainMenu : Control
|
||||||
|
{
|
||||||
|
private Button _startButton = null!;
|
||||||
|
private Button _settingsButton = null!;
|
||||||
|
private Button _quitButton = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
|
private void OnStartButtonPressed()
|
||||||
|
{
|
||||||
|
StartGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
|
||||||
|
private void OnSettingsButtonPressed()
|
||||||
|
{
|
||||||
|
ShowSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
|
||||||
|
private void OnQuitButtonPressed()
|
||||||
|
{
|
||||||
|
QuitGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成的代码
|
||||||
|
|
||||||
|
编译器会为标记的类自动生成以下代码:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace YourNamespace;
|
||||||
|
|
||||||
|
partial class MainMenu
|
||||||
|
{
|
||||||
|
private void __BindNodeSignals_Generated()
|
||||||
|
{
|
||||||
|
_startButton.Pressed += OnStartButtonPressed;
|
||||||
|
_settingsButton.Pressed += OnSettingsButtonPressed;
|
||||||
|
_quitButton.Pressed += OnQuitButtonPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void __UnbindNodeSignals_Generated()
|
||||||
|
{
|
||||||
|
_startButton.Pressed -= OnStartButtonPressed;
|
||||||
|
_settingsButton.Pressed -= OnSettingsButtonPressed;
|
||||||
|
_quitButton.Pressed -= OnQuitButtonPressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数说明
|
||||||
|
|
||||||
|
`[BindNodeSignal]` 特性需要两个参数:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|-----------------|--------|-----------------------------|
|
||||||
|
| `nodeFieldName` | string | 目标节点字段名(使用 `nameof` 推荐) |
|
||||||
|
| `signalName` | string | 目标节点上的 CLR 事件名(使用 `nameof`) |
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[BindNodeSignal("_startButton", "Pressed")] // 字符串字面量
|
||||||
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] // 推荐:nameof 表达式
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 带参数的事件处理
|
||||||
|
|
||||||
|
处理带参数的事件(如 `SpinBox.ValueChanged`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class SettingsPanel : Control
|
||||||
|
{
|
||||||
|
private SpinBox _volumeSpinBox = null!;
|
||||||
|
private SpinBox _brightnessSpinBox = null!;
|
||||||
|
|
||||||
|
// 参数类型必须与事件委托匹配
|
||||||
|
[BindNodeSignal(nameof(_volumeSpinBox), nameof(SpinBox.ValueChanged))]
|
||||||
|
private void OnVolumeChanged(double value)
|
||||||
|
{
|
||||||
|
SetVolume((float)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_brightnessSpinBox), nameof(SpinBox.ValueChanged))]
|
||||||
|
private void OnBrightnessChanged(double value)
|
||||||
|
{
|
||||||
|
SetBrightness((float)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多事件绑定
|
||||||
|
|
||||||
|
一个方法可以同时绑定到多个节点的事件:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class MultiButtonHud : Control
|
||||||
|
{
|
||||||
|
private Button _buttonA = null!;
|
||||||
|
private Button _buttonB = null!;
|
||||||
|
private Button _buttonC = null!;
|
||||||
|
|
||||||
|
// 一个方法处理多个按钮的点击
|
||||||
|
[BindNodeSignal(nameof(_buttonA), nameof(Button.Pressed))]
|
||||||
|
[BindNodeSignal(nameof(_buttonB), nameof(Button.Pressed))]
|
||||||
|
[BindNodeSignal(nameof(_buttonC), nameof(Button.Pressed))]
|
||||||
|
private void OnAnyButtonPressed()
|
||||||
|
{
|
||||||
|
PlayClickSound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 与 [GetNode] 组合使用
|
||||||
|
|
||||||
|
推荐与 `[GetNode]` 特性结合使用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class GameHud : Control
|
||||||
|
{
|
||||||
|
// 使用 GetNode 自动获取节点
|
||||||
|
[GetNode]
|
||||||
|
private Button _pauseButton = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private ProgressBar _healthBar = null!;
|
||||||
|
|
||||||
|
[GetNode("UI/ScoreLabel")]
|
||||||
|
private Label _scoreLabel = null!;
|
||||||
|
|
||||||
|
// 使用 BindNodeSignal 绑定事件
|
||||||
|
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
|
||||||
|
private void OnPauseButtonPressed()
|
||||||
|
{
|
||||||
|
TogglePause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多事件绑定示例
|
||||||
|
[BindNodeSignal(nameof(_healthBar), nameof(Range.ValueChanged))]
|
||||||
|
private void OnHealthChanged(double value)
|
||||||
|
{
|
||||||
|
UpdateHealthDisplay(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 先注入节点,再绑定信号
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 复杂事件处理场景
|
||||||
|
|
||||||
|
实现完整的 UI 事件处理:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class InventoryUI : Control
|
||||||
|
{
|
||||||
|
// 节点
|
||||||
|
[GetNode]
|
||||||
|
private ItemList _itemList = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private Button _useButton = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private Button _dropButton = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private LineEdit _searchBox = null!;
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemSelected))]
|
||||||
|
private void OnItemSelected(long index)
|
||||||
|
{
|
||||||
|
SelectItem((int)index);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_itemList), nameof(ItemList.ItemActivated))]
|
||||||
|
private void OnItemActivated(long index)
|
||||||
|
{
|
||||||
|
UseItem((int)index);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_useButton), nameof(Button.Pressed))]
|
||||||
|
private void OnUseButtonPressed()
|
||||||
|
{
|
||||||
|
UseSelectedItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_dropButton), nameof(Button.Pressed))]
|
||||||
|
private void OnDropButtonPressed()
|
||||||
|
{
|
||||||
|
DropSelectedItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_searchBox), nameof(LineEdit.TextChanged))]
|
||||||
|
private void OnSearchTextChanged(string newText)
|
||||||
|
{
|
||||||
|
FilterItems(newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
InitializeInventory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生命周期管理
|
||||||
|
|
||||||
|
### 自动生成生命周期方法
|
||||||
|
|
||||||
|
如果类没有 `_Ready()` 或 `_ExitTree()`,生成器会自动生成:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class AutoLifecycleHud : Control
|
||||||
|
{
|
||||||
|
private Button _button = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnButtonPressed()
|
||||||
|
{
|
||||||
|
// 处理点击
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无需手动声明 _Ready 和 _ExitTree
|
||||||
|
// 生成器会自动生成:
|
||||||
|
// public override void _Ready() { __BindNodeSignals_Generated(); }
|
||||||
|
// public override void _ExitTree() { __UnbindNodeSignals_Generated(); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动生命周期调用
|
||||||
|
|
||||||
|
如果已有生命周期方法,需要手动调用生成的方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class CustomLifecycleHud : Control
|
||||||
|
{
|
||||||
|
private Button _button = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnButtonPressed()
|
||||||
|
{
|
||||||
|
HandlePress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 必须手动调用绑定方法
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
|
||||||
|
// 自定义初始化逻辑
|
||||||
|
InitializeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
// 必须手动调用解绑方法
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
|
||||||
|
// 自定义清理逻辑
|
||||||
|
CleanupResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:如果在 `_Ready()` 中不调用 `__BindNodeSignals_Generated()`,编译器会发出警告 `GF_Godot_BindNodeSignal_008`。
|
||||||
|
|
||||||
|
## 诊断信息
|
||||||
|
|
||||||
|
生成器会在以下情况报告编译错误或警告:
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_001 - 不支持嵌套类
|
||||||
|
|
||||||
|
**错误信息**:`Class '{ClassName}' cannot use [BindNodeSignal] inside a nested type`
|
||||||
|
|
||||||
|
**解决方案**:将嵌套类提取为独立的类
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
public partial class Outer
|
||||||
|
{
|
||||||
|
public partial class Inner
|
||||||
|
{
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnPressed() { } // 错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
public partial class Inner
|
||||||
|
{
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnPressed() { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_002 - 不支持静态方法
|
||||||
|
|
||||||
|
**错误信息**:`Method '{MethodName}' cannot be static when using [BindNodeSignal]`
|
||||||
|
|
||||||
|
**解决方案**:改为实例方法
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private static void OnPressed() { }
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnPressed() { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_003 - 节点字段不存在
|
||||||
|
|
||||||
|
**错误信息**:
|
||||||
|
`Method '{MethodName}' references node field '{FieldName}', but no matching field exists on class '{ClassName}'`
|
||||||
|
|
||||||
|
**解决方案**:确保引用的字段存在且名称正确
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误:_button 字段不存在
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnPressed() { }
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
private Button _button = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnPressed() { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_004 - 节点字段必须是实例字段
|
||||||
|
|
||||||
|
**错误信息**:`Method '{MethodName}' references node field '{FieldName}', but that field must be an instance field`
|
||||||
|
|
||||||
|
**解决方案**:将节点字段改为实例字段(非静态)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
private static Button _button = null!;
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
private Button _button = null!;
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_005 - 字段类型必须继承自 Godot.Node
|
||||||
|
|
||||||
|
**错误信息**:`Field '{FieldName}' must be a Godot.Node type to use [BindNodeSignal]`
|
||||||
|
|
||||||
|
**解决方案**:确保字段类型继承自 `Godot.Node`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
private string _text = null!; // string 不是 Node 类型
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_text), "Changed")] // 错误
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
private Button _button = null!; // Button 继承自 Node
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_006 - 目标事件不存在
|
||||||
|
|
||||||
|
**错误信息**:`Field '{FieldName}' does not contain an event named '{EventName}'`
|
||||||
|
|
||||||
|
**解决方案**:确保事件名称正确
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private Button _button = null!;
|
||||||
|
|
||||||
|
// ❌ 错误:Click 不是 Button 的事件
|
||||||
|
[BindNodeSignal(nameof(_button), "Click")]
|
||||||
|
|
||||||
|
// ✅ 正确:使用正确的事件名
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_007 - 方法签名不兼容
|
||||||
|
|
||||||
|
**错误信息**:`Method '{MethodName}' is not compatible with event '{EventName}' on field '{FieldName}'`
|
||||||
|
|
||||||
|
**解决方案**:确保方法签名与事件委托匹配
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private SpinBox _spinBox = null!;
|
||||||
|
|
||||||
|
// ❌ 错误:SpinBox.ValueChanged 需要 double 参数
|
||||||
|
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
|
||||||
|
private void OnValueChanged() { } // 缺少参数
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[BindNodeSignal(nameof(_spinBox), nameof(SpinBox.ValueChanged))]
|
||||||
|
private void OnValueChanged(double value) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_008 - 需要在 _Ready 中调用绑定方法
|
||||||
|
|
||||||
|
**警告信息**:
|
||||||
|
`Class '{ClassName}' defines _Ready(); call __BindNodeSignals_Generated() there to bind [BindNodeSignal] handlers`
|
||||||
|
|
||||||
|
**解决方案**:在 `_Ready()` 中手动调用 `__BindNodeSignals_Generated()`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__BindNodeSignals_Generated(); // ✅ 必须手动调用
|
||||||
|
// 其他初始化...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_009 - 需要在 _ExitTree 中调用解绑方法
|
||||||
|
|
||||||
|
**警告信息**:
|
||||||
|
`Class '{ClassName}' defines _ExitTree(); call __UnbindNodeSignals_Generated() there to unbind [BindNodeSignal] handlers`
|
||||||
|
|
||||||
|
**解决方案**:在 `_ExitTree()` 中手动调用 `__UnbindNodeSignals_Generated()`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated(); // ✅ 必须手动调用
|
||||||
|
// 其他清理...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_BindNodeSignal_010 - 构造参数无效
|
||||||
|
|
||||||
|
**错误信息**:
|
||||||
|
`Method '{MethodName}' uses [BindNodeSignal] with an invalid '{ParameterName}' constructor argument; it must be a non-empty string literal`
|
||||||
|
|
||||||
|
**解决方案**:使用有效的字符串字面量或 nameof 表达式
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误:空字符串
|
||||||
|
[BindNodeSignal("", nameof(Button.Pressed))]
|
||||||
|
|
||||||
|
// ❌ 错误:null 值
|
||||||
|
[BindNodeSignal(null, nameof(Button.Pressed))]
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用 nameof 表达式
|
||||||
|
|
||||||
|
使用 `nameof` 而不是字符串字面量,以获得重构支持和编译时检查:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 不推荐:字符串字面量
|
||||||
|
[BindNodeSignal("_button", "Pressed")]
|
||||||
|
|
||||||
|
// ✅ 推荐:nameof 表达式
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 保持方法命名一致
|
||||||
|
|
||||||
|
使用统一的命名约定提高代码可读性:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:On + 节点名 + 事件名
|
||||||
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
|
private void OnStartButtonPressed() { }
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_volumeSlider), nameof(Slider.ValueChanged))]
|
||||||
|
private void OnVolumeSliderValueChanged(double value) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分组相关事件处理
|
||||||
|
|
||||||
|
将相关的事件处理方法放在一起,便于维护:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class GameHud : Control
|
||||||
|
{
|
||||||
|
// UI 节点
|
||||||
|
[GetNode]
|
||||||
|
private Button _pauseButton = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private Button _menuButton = null!;
|
||||||
|
|
||||||
|
// UI 事件处理(放在一起)
|
||||||
|
[BindNodeSignal(nameof(_pauseButton), nameof(Button.Pressed))]
|
||||||
|
private void OnPauseButtonPressed() { }
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_menuButton), nameof(Button.Pressed))]
|
||||||
|
private void OnMenuButtonPressed() { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 正确处理生命周期
|
||||||
|
|
||||||
|
始终确保事件解绑,避免内存泄漏:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class SafeHud : Control
|
||||||
|
{
|
||||||
|
private Button _button = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_button), nameof(Button.Pressed))]
|
||||||
|
private void OnButtonPressed() { }
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
// 确保解绑事件
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 对比手动事件绑定
|
||||||
|
|
||||||
|
| 方式 | 代码量 | 可维护性 | 错误风险 | 推荐场景 |
|
||||||
|
|--------------------|-----|------|----------|------------|
|
||||||
|
| 手动 `+=` / `-=` | 多 | 中 | 高(易遗漏解绑) | 简单场景 |
|
||||||
|
| `[BindNodeSignal]` | 少 | 高 | 低(编译器检查) | 复杂 UI、频繁事件 |
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 不推荐:手动绑定
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_startButton.Pressed += OnStartButtonPressed;
|
||||||
|
_settingsButton.Pressed += OnSettingsButtonPressed;
|
||||||
|
_quitButton.Pressed += OnQuitButtonPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
// 容易遗漏解绑
|
||||||
|
_startButton.Pressed -= OnStartButtonPressed;
|
||||||
|
_quitButton.Pressed -= OnQuitButtonPressed; // 遗漏了 _settingsButton
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:使用 [BindNodeSignal]
|
||||||
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
|
private void OnStartButtonPressed() { }
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_settingsButton), nameof(Button.Pressed))]
|
||||||
|
private void OnSettingsButtonPressed() { }
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_quitButton), nameof(Button.Pressed))]
|
||||||
|
private void OnQuitButtonPressed() { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 与 [ContextAware] 组合使用
|
||||||
|
|
||||||
|
在需要架构访问的场景中,与 `[ContextAware]` 结合:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
|
||||||
|
[ContextAware]
|
||||||
|
public partial class GameController : Node
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Button _actionButton = null!;
|
||||||
|
|
||||||
|
private IGameModel _gameModel = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_actionButton), nameof(Button.Pressed))]
|
||||||
|
private void OnActionButtonPressed()
|
||||||
|
{
|
||||||
|
// 可以直接使用架构功能
|
||||||
|
this.SendCommand(new PlayerActionCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectContextBindings_Generated();
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [Source Generators 概述](./index)
|
||||||
|
- [GetNode 生成器](./get-node-generator)
|
||||||
|
- [ContextAware 生成器](./context-aware-generator)
|
||||||
|
- [Godot 信号文档](https://docs.godotengine.org/en/stable/classes/class_signal.html)
|
||||||
496
docs/zh-CN/source-generators/get-node-generator.md
Normal file
496
docs/zh-CN/source-generators/get-node-generator.md
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
# GetNode 生成器
|
||||||
|
|
||||||
|
> 自动生成 Godot 节点获取逻辑,简化节点引用代码
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode<T>()` 方法。这在处理复杂
|
||||||
|
UI 或场景树结构时特别有用。
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
- **自动节点获取**:根据路径或字段名自动获取节点
|
||||||
|
- **多种查找模式**:支持唯一名、相对路径、绝对路径查找
|
||||||
|
- **可选节点支持**:可以标记节点为可选,获取失败时返回 null
|
||||||
|
- **智能路径推导**:未显式指定路径时自动从字段名推导
|
||||||
|
- **_Ready 钩子生成**:自动生成 `_Ready()` 方法注入节点获取逻辑
|
||||||
|
|
||||||
|
## 基础使用
|
||||||
|
|
||||||
|
### 标记节点字段
|
||||||
|
|
||||||
|
使用 `[GetNode]` 特性标记需要自动获取的节点字段:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class PlayerHud : Control
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _healthLabel = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private ProgressBar _manaBar = null!;
|
||||||
|
|
||||||
|
[GetNode("ScoreContainer/ScoreValue")]
|
||||||
|
private Label _scoreLabel = null!;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
_healthLabel.Text = "100";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成的代码
|
||||||
|
|
||||||
|
编译器会为标记的类自动生成以下代码:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace YourNamespace;
|
||||||
|
|
||||||
|
partial class PlayerHud
|
||||||
|
{
|
||||||
|
private void __InjectGetNodes_Generated()
|
||||||
|
{
|
||||||
|
_healthLabel = GetNode<global::Godot.Label>("%HealthLabel");
|
||||||
|
_manaBar = GetNode<global::Godot.ProgressBar>("%ManaBar");
|
||||||
|
_scoreLabel = GetNode<global::Godot.Label>("ScoreContainer/ScoreValue");
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnGetNodeReadyGenerated();
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
OnGetNodeReadyGenerated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### 节点查找模式
|
||||||
|
|
||||||
|
通过 `Lookup` 参数控制节点查找方式:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class GameHud : Control
|
||||||
|
{
|
||||||
|
// 自动推断(默认):根据路径前缀自动选择
|
||||||
|
[GetNode]
|
||||||
|
private Label _titleLabel = null!; // 默认使用唯一名 %TitleLabel
|
||||||
|
|
||||||
|
// 唯一名查找
|
||||||
|
[GetNode(Lookup = NodeLookupMode.UniqueName)]
|
||||||
|
private Button _startButton = null!; // %StartButton
|
||||||
|
|
||||||
|
// 相对路径查找
|
||||||
|
[GetNode("UI/HealthBar", Lookup = NodeLookupMode.RelativePath)]
|
||||||
|
private ProgressBar _healthBar = null!;
|
||||||
|
|
||||||
|
// 绝对路径查找
|
||||||
|
[GetNode("/root/Main/GameUI/Score", Lookup = NodeLookupMode.AbsolutePath)]
|
||||||
|
private Label _scoreLabel = null!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查找模式说明
|
||||||
|
|
||||||
|
| 模式 | 路径前缀 | 适用场景 |
|
||||||
|
|----------------|------|----------------|
|
||||||
|
| `Auto` | 自动选择 | 默认行为,推荐用于大多数场景 |
|
||||||
|
| `UniqueName` | `%` | 场景中使用唯一名的节点 |
|
||||||
|
| `RelativePath` | 无 | 需要相对路径查找的节点 |
|
||||||
|
| `AbsolutePath` | `/` | 场景树根节点的绝对路径 |
|
||||||
|
|
||||||
|
### 可选节点
|
||||||
|
|
||||||
|
对于可能不存在的节点,可以设置为非必填:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class SettingsPanel : Control
|
||||||
|
{
|
||||||
|
// 必须存在的节点(默认)
|
||||||
|
[GetNode]
|
||||||
|
private Label _titleLabel = null!;
|
||||||
|
|
||||||
|
// 可选节点,可能不存在
|
||||||
|
[GetNode(Required = false)]
|
||||||
|
private Label? _debugLabel; // 使用可空类型
|
||||||
|
|
||||||
|
// 显式路径的可选节点
|
||||||
|
[GetNode("AdvancedOptions", Required = false)]
|
||||||
|
private VBoxContainer? _advancedOptions;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
|
||||||
|
// 安全地访问可选节点
|
||||||
|
_debugLabel?.Hide();
|
||||||
|
_advancedOptions?.Hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路径规则
|
||||||
|
|
||||||
|
生成器根据字段名和配置自动推导节点路径:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class Example : Control
|
||||||
|
{
|
||||||
|
// 驼峰命名 → PascalCase 路径
|
||||||
|
[GetNode]
|
||||||
|
private Label _playerNameLabel = null!; // → %PlayerNameLabel
|
||||||
|
|
||||||
|
// m_ 前缀会被移除
|
||||||
|
[GetNode]
|
||||||
|
private Button m_confirmButton = null!; // → %ConfirmButton
|
||||||
|
|
||||||
|
// _ 前缀会被移除
|
||||||
|
[GetNode]
|
||||||
|
private ProgressBar _healthBar = null!; // → %HealthBar
|
||||||
|
|
||||||
|
// 显式路径优先于推导
|
||||||
|
[GetNode("UI/CustomPath")]
|
||||||
|
private Label _myLabel = null!; // → UI/CustomPath
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 与 [ContextAware] 组合使用
|
||||||
|
|
||||||
|
在 Godot 项目中结合使用 `[GetNode]` 和 `[ContextAware]`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
[ContextAware]
|
||||||
|
public partial class GameController : Node
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _scoreLabel = null!;
|
||||||
|
|
||||||
|
[GetNode("HUD/HealthBar")]
|
||||||
|
private ProgressBar _healthBar = null!;
|
||||||
|
|
||||||
|
private IGameModel _gameModel = null!;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectContextBindings_Generated(); // ContextAware 生成
|
||||||
|
__InjectGetNodes_Generated(); // GetNode 生成
|
||||||
|
|
||||||
|
_gameModel.Score.Register(OnScoreChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnScoreChanged(int newScore)
|
||||||
|
{
|
||||||
|
_scoreLabel.Text = newScore.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 复杂 UI 场景
|
||||||
|
|
||||||
|
处理复杂的嵌套 UI 结构:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class InventoryUI : Control
|
||||||
|
{
|
||||||
|
// 主容器
|
||||||
|
[GetNode]
|
||||||
|
private GridContainer _itemGrid = null!;
|
||||||
|
|
||||||
|
// 详细信息面板
|
||||||
|
[GetNode("DetailsPanel/ItemName")]
|
||||||
|
private Label _itemNameLabel = null!;
|
||||||
|
|
||||||
|
[GetNode("DetailsPanel/ItemDescription")]
|
||||||
|
private RichTextLabel _itemDescription = null!;
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
|
[GetNode("Actions/UseButton")]
|
||||||
|
private Button _useButton = null!;
|
||||||
|
|
||||||
|
[GetNode("Actions/DropButton")]
|
||||||
|
private Button _dropButton = null!;
|
||||||
|
|
||||||
|
// 可选的统计信息
|
||||||
|
[GetNode("DetailsPanel/Stats", Required = false)]
|
||||||
|
private VBoxContainer? _statsContainer;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
|
||||||
|
// 使用注入的节点
|
||||||
|
_useButton.Pressed += OnUseButtonPressed;
|
||||||
|
_dropButton.Pressed += OnDropButtonPressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动 _Ready 调用
|
||||||
|
|
||||||
|
如果类已经有 `_Ready()` 方法,需要手动调用注入方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class CustomHud : Control
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _statusLabel = null!;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// 必须手动调用节点注入
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
|
||||||
|
// 自定义初始化逻辑
|
||||||
|
_statusLabel.Text = "Ready";
|
||||||
|
InitializeOtherComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnGetNodeReadyGenerated()
|
||||||
|
{
|
||||||
|
// 这个方法会被生成器调用,可以在此添加额外初始化
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:如果不手动调用 `__InjectGetNodes_Generated()`,编译器会发出警告 `GF_Godot_GetNode_006`。
|
||||||
|
|
||||||
|
## 诊断信息
|
||||||
|
|
||||||
|
生成器会在以下情况报告编译错误或警告:
|
||||||
|
|
||||||
|
### GF_Godot_GetNode_001 - 不支持嵌套类
|
||||||
|
|
||||||
|
**错误信息**:`Class '{ClassName}' cannot use [GetNode] inside a nested type`
|
||||||
|
|
||||||
|
**解决方案**:将嵌套类提取为独立的类
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
public partial class Outer
|
||||||
|
{
|
||||||
|
public partial class Inner
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _label = null!; // 错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
public partial class Inner
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _label = null!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_GetNode_002 - 不支持静态字段
|
||||||
|
|
||||||
|
**错误信息**:`Field '{FieldName}' cannot be static when using [GetNode]`
|
||||||
|
|
||||||
|
**解决方案**:改为实例字段
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
[GetNode]
|
||||||
|
private static Label _label = null!;
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[GetNode]
|
||||||
|
private Label _label = null!;
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_GetNode_003 - 不支持只读字段
|
||||||
|
|
||||||
|
**错误信息**:`Field '{FieldName}' cannot be readonly when using [GetNode]`
|
||||||
|
|
||||||
|
**解决方案**:移除 `readonly` 关键字
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
[GetNode]
|
||||||
|
private readonly Label _label = null!;
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[GetNode]
|
||||||
|
private Label _label = null!;
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_GetNode_004 - 字段类型必须继承自 Godot.Node
|
||||||
|
|
||||||
|
**错误信息**:`Field '{FieldName}' must be a Godot.Node type to use [GetNode]`
|
||||||
|
|
||||||
|
**解决方案**:确保字段类型继承自 `Godot.Node`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误
|
||||||
|
[GetNode]
|
||||||
|
private string _text = null!; // string 不是 Node 类型
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[GetNode]
|
||||||
|
private Label _label = null!; // Label 继承自 Node
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_GetNode_005 - 无法推导路径
|
||||||
|
|
||||||
|
**错误信息**:`Field '{FieldName}' does not provide a path and its name cannot be converted to a node path`
|
||||||
|
|
||||||
|
**解决方案**:显式指定节点路径
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 错误:字段名无法转换为有效路径
|
||||||
|
[GetNode]
|
||||||
|
private Label _ = null!;
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
[GetNode("UI/Label")]
|
||||||
|
private Label _ = null!;
|
||||||
|
```
|
||||||
|
|
||||||
|
### GF_Godot_GetNode_006 - 需要在 _Ready 中调用注入方法
|
||||||
|
|
||||||
|
**警告信息**:
|
||||||
|
`Class '{ClassName}' defines _Ready(); call __InjectGetNodes_Generated() there or remove _Ready() to use the generated hook`
|
||||||
|
|
||||||
|
**解决方案**:在 `_Ready()` 中手动调用 `__InjectGetNodes_Generated()`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class MyHud : Control
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _label = null!;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated(); // ✅ 必须手动调用
|
||||||
|
// 其他初始化...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用一致的命名约定
|
||||||
|
|
||||||
|
保持字段名与场景树中节点名的一致性:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ✅ 推荐:字段名与节点名一致
|
||||||
|
[GetNode]
|
||||||
|
private Label _healthLabel = null!; // 场景中的节点名为 HealthLabel
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private Button _startButton = null!; // 场景中的节点名为 StartButton
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 优先使用唯一名查找
|
||||||
|
|
||||||
|
在 Godot 编辑器中为重要节点启用唯一名(Unique Name),然后使用 `[GetNode]`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Godot 场景中:%HealthBar(唯一名已启用)
|
||||||
|
// C# 代码中:
|
||||||
|
[GetNode]
|
||||||
|
private ProgressBar _healthBar = null!; // 自动使用 %HealthBar
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 合理处理可选节点
|
||||||
|
|
||||||
|
对于可能不存在的节点,使用 `Required = false`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class DynamicUI : Control
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _titleLabel = null!;
|
||||||
|
|
||||||
|
// 可选组件
|
||||||
|
[GetNode(Required = false)]
|
||||||
|
private TextureRect? _iconImage;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
|
||||||
|
// 安全地初始化可选组件
|
||||||
|
if (_iconImage != null)
|
||||||
|
{
|
||||||
|
_iconImage.Texture = LoadDefaultIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 组织复杂 UI 的路径
|
||||||
|
|
||||||
|
对于深层嵌套的 UI,使用显式路径:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class ComplexUI : Control
|
||||||
|
{
|
||||||
|
// 使用相对路径明确表达层级关系
|
||||||
|
[GetNode("MainContent/Header/Title")]
|
||||||
|
private Label _title = null!;
|
||||||
|
|
||||||
|
[GetNode("MainContent/Body/Stats/Health")]
|
||||||
|
private Label _healthValue = null!;
|
||||||
|
|
||||||
|
[GetNode("MainContent/Footer/ActionButtons/Save")]
|
||||||
|
private Button _saveButton = null!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 与 GetNode 方法的对比
|
||||||
|
|
||||||
|
| 方式 | 代码量 | 可维护性 | 类型安全 | 推荐场景 |
|
||||||
|
|----------------|-----|------|--------|-----------|
|
||||||
|
| 手动 `GetNode()` | 多 | 中 | 需要显式转换 | 简单场景 |
|
||||||
|
| `[GetNode]` 特性 | 少 | 高 | 编译时检查 | 复杂 UI、控制器 |
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 不推荐:手动获取
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_healthLabel = GetNode<Label>("%HealthLabel");
|
||||||
|
_manaBar = GetNode<ProgressBar>("%ManaBar");
|
||||||
|
_scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:使用 [GetNode] 特性
|
||||||
|
[GetNode]
|
||||||
|
private Label _healthLabel = null!;
|
||||||
|
|
||||||
|
[GetNode]
|
||||||
|
private ProgressBar _manaBar = null!;
|
||||||
|
|
||||||
|
[GetNode("ScoreContainer/ScoreValue")]
|
||||||
|
private Label _scoreLabel = null!;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [Source Generators 概述](./index)
|
||||||
|
- [BindNodeSignal 生成器](./bind-node-signal-generator)
|
||||||
|
- [ContextAware 生成器](./context-aware-generator)
|
||||||
|
- [Godot 节点文档](https://docs.godotengine.org/en/stable/classes/class_node.html)
|
||||||
@ -14,6 +14,8 @@ GFramework.SourceGenerators 是 GFramework 框架的源代码生成器包,通
|
|||||||
- [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器)
|
- [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器)
|
||||||
- [Priority 属性生成器](#priority-属性生成器)
|
- [Priority 属性生成器](#priority-属性生成器)
|
||||||
- [Context Get 注入生成器](#context-get-注入生成器)
|
- [Context Get 注入生成器](#context-get-注入生成器)
|
||||||
|
- [GetNode 生成器 (Godot)](#getnode-生成器)
|
||||||
|
- [BindNodeSignal 生成器 (Godot)](#bindnodesignal-生成器)
|
||||||
- [诊断信息](#诊断信息)
|
- [诊断信息](#诊断信息)
|
||||||
- [性能优势](#性能优势)
|
- [性能优势](#性能优势)
|
||||||
- [使用示例](#使用示例)
|
- [使用示例](#使用示例)
|
||||||
@ -41,6 +43,11 @@ GFramework.SourceGenerators 利用 Roslyn 源代码生成器技术,在编译
|
|||||||
- **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记
|
- **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记
|
||||||
- **Context Get 注入特性**:自动注入架构组件(GetModel/GetSystem/GetUtility/GetService/GetAll)
|
- **Context Get 注入特性**:自动注入架构组件(GetModel/GetSystem/GetUtility/GetService/GetAll)
|
||||||
|
|
||||||
|
### Godot 专用生成器
|
||||||
|
|
||||||
|
- **[GetNode] 属性 (Godot)**:自动获取 Godot 节点引用,支持多种查找模式
|
||||||
|
- **[BindNodeSignal] 属性 (Godot)**:自动生成 Godot 节点信号绑定与解绑逻辑
|
||||||
|
|
||||||
### 🔧 高级特性
|
### 🔧 高级特性
|
||||||
|
|
||||||
- **智能诊断**:生成器包含详细的错误诊断信息
|
- **智能诊断**:生成器包含详细的错误诊断信息
|
||||||
@ -379,6 +386,82 @@ public enum PlayerState
|
|||||||
| GenerateIsMethods | bool | true | 是否为每个枚举值生成 IsX 方法 |
|
| GenerateIsMethods | bool | true | 是否为每个枚举值生成 IsX 方法 |
|
||||||
| GenerateIsInMethod | bool | true | 是否生成 IsIn 方法 |
|
| GenerateIsInMethod | bool | true | 是否生成 IsIn 方法 |
|
||||||
|
|
||||||
|
## GetNode 生成器
|
||||||
|
|
||||||
|
GetNode 生成器为标记了 `[GetNode]` 特性的字段自动生成 Godot 节点获取代码,无需手动调用 `GetNode<T>()` 方法。
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
|
||||||
|
- **自动节点获取**:根据路径或字段名自动获取 Godot 节点
|
||||||
|
- **多种查找模式**:支持唯一名(`%Name`)、相对路径、绝对路径查找
|
||||||
|
- **可选节点支持**:可以标记节点为可选,获取失败时返回 null
|
||||||
|
- **_Ready 钩子**:自动生成 `_Ready()` 方法注入节点获取逻辑
|
||||||
|
|
||||||
|
### 基础示例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class PlayerHud : Control
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private Label _healthLabel = null!;
|
||||||
|
|
||||||
|
[GetNode("HUD/ScoreValue")]
|
||||||
|
private Label _scoreLabel = null!;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__InjectGetNodes_Generated();
|
||||||
|
_healthLabel.Text = "100";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**完整文档**:[GetNode 生成器](./get-node-generator)
|
||||||
|
|
||||||
|
## BindNodeSignal 生成器
|
||||||
|
|
||||||
|
BindNodeSignal 生成器为标记了 `[BindNodeSignal]` 特性的方法自动生成节点事件绑定和解绑代码。
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
|
||||||
|
- **自动事件绑定**:在 `_Ready()` 中自动订阅节点事件
|
||||||
|
- **自动事件解绑**:在 `_ExitTree()` 中自动取消订阅
|
||||||
|
- **多事件绑定**:一个方法可以绑定到多个节点事件
|
||||||
|
- **类型安全检查**:编译时验证方法签名与事件委托的兼容性
|
||||||
|
|
||||||
|
### 基础示例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class MainMenu : Control
|
||||||
|
{
|
||||||
|
private Button _startButton = null!;
|
||||||
|
|
||||||
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
|
private void OnStartButtonPressed()
|
||||||
|
{
|
||||||
|
StartGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
__UnbindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**完整文档**:[BindNodeSignal 生成器](./bind-node-signal-generator)
|
||||||
|
|
||||||
## 诊断信息
|
## 诊断信息
|
||||||
|
|
||||||
GFramework.SourceGenerators 提供详细的编译时诊断信息,帮助开发者快速定位和解决问题。
|
GFramework.SourceGenerators 提供详细的编译时诊断信息,帮助开发者快速定位和解决问题。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user