mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-31 18:39:00 +08:00
feat(generator): 添加 BindNodeSignal 源生成器实现
- 实现 BindNodeSignalGenerator 源生成器,用于自动生成 Godot 节点事件绑定与解绑逻辑 - 添加 BindNodeSignalAttribute 特性,标记需要生成绑定逻辑的事件处理方法 - 实现完整的诊断系统,包括嵌套类型、静态方法、字段类型等错误检查 - 添加生命周期方法调用检查,在 _Ready 和 _ExitTree 中验证生成方法的调用 - 支持方法签名与事件委托的兼容性验证 - 实现单元测试覆盖各种使用场景和错误情况
This commit is contained in:
parent
a628ade28e
commit
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,467 @@
|
|||||||
|
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_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();
|
||||||
|
}
|
||||||
|
}
|
||||||
523
GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
Normal file
523
GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
using GFramework.Godot.SourceGenerators.Diagnostics;
|
||||||
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
|
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
|
|
||||||
|
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 => attribute.Name.ToString().Contains("BindNodeSignal", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodCandidate? Transform(GeneratorSyntaxContext context)
|
||||||
|
{
|
||||||
|
if (context.Node is not MethodDeclarationSyntax methodDeclaration)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is not IMethodSymbol methodSymbol)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new MethodCandidate(methodDeclaration, methodSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Execute(
|
||||||
|
SourceProductionContext context,
|
||||||
|
Compilation compilation,
|
||||||
|
ImmutableArray<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 methodCandidates = candidates
|
||||||
|
.Where(static candidate => candidate is not null)
|
||||||
|
.Select(static candidate => candidate!)
|
||||||
|
.Where(candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute).Count > 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var group in GroupByContainingType(methodCandidates))
|
||||||
|
{
|
||||||
|
var typeSymbol = group.TypeSymbol;
|
||||||
|
if (!CanGenerateForType(context, group, typeSymbol))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var bindings = new List<SignalBindingInfo>();
|
||||||
|
|
||||||
|
foreach (var candidate in group.Methods)
|
||||||
|
{
|
||||||
|
foreach (var attribute in ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute))
|
||||||
|
{
|
||||||
|
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bindings.Add(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindings.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ReportMissingLifecycleHookCall(
|
||||||
|
context,
|
||||||
|
group,
|
||||||
|
typeSymbol,
|
||||||
|
"_Ready",
|
||||||
|
BindMethodName,
|
||||||
|
BindNodeSignalDiagnostics.ManualReadyHookRequired);
|
||||||
|
|
||||||
|
ReportMissingLifecycleHookCall(
|
||||||
|
context,
|
||||||
|
group,
|
||||||
|
typeSymbol,
|
||||||
|
"_ExitTree",
|
||||||
|
UnbindMethodName,
|
||||||
|
BindNodeSignalDiagnostics.ManualExitTreeHookRequired);
|
||||||
|
|
||||||
|
context.AddSource(GetHintName(typeSymbol), GenerateSource(typeSymbol, bindings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanGenerateForType(
|
||||||
|
SourceProductionContext context,
|
||||||
|
TypeGroup group,
|
||||||
|
INamedTypeSymbol typeSymbol)
|
||||||
|
{
|
||||||
|
if (typeSymbol.ContainingType is not null)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
BindNodeSignalDiagnostics.NestedClassNotSupported,
|
||||||
|
group.Methods[0].Method.Identifier.GetLocation(),
|
||||||
|
typeSymbol.Name));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeSymbol.AreAllDeclarationsPartial())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
CommonDiagnostics.ClassMustBePartial,
|
||||||
|
group.Methods[0].Method.Identifier.GetLocation(),
|
||||||
|
typeSymbol.Name));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCreateBinding(
|
||||||
|
SourceProductionContext context,
|
||||||
|
MethodCandidate candidate,
|
||||||
|
AttributeData attribute,
|
||||||
|
INamedTypeSymbol godotNodeSymbol,
|
||||||
|
out SignalBindingInfo binding)
|
||||||
|
{
|
||||||
|
binding = default!;
|
||||||
|
|
||||||
|
if (candidate.MethodSymbol.IsStatic)
|
||||||
|
{
|
||||||
|
ReportMethodDiagnostic(
|
||||||
|
context,
|
||||||
|
BindNodeSignalDiagnostics.StaticMethodNotSupported,
|
||||||
|
candidate,
|
||||||
|
attribute,
|
||||||
|
candidate.MethodSymbol.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeFieldName = ResolveCtorString(attribute, 0);
|
||||||
|
var signalName = ResolveCtorString(attribute, 1);
|
||||||
|
|
||||||
|
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
|
||||||
|
if (fieldSymbol is null)
|
||||||
|
{
|
||||||
|
ReportMethodDiagnostic(
|
||||||
|
context,
|
||||||
|
BindNodeSignalDiagnostics.NodeFieldNotFound,
|
||||||
|
candidate,
|
||||||
|
attribute,
|
||||||
|
candidate.MethodSymbol.Name,
|
||||||
|
nodeFieldName,
|
||||||
|
candidate.MethodSymbol.ContainingType.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldSymbol.IsStatic)
|
||||||
|
{
|
||||||
|
ReportMethodDiagnostic(
|
||||||
|
context,
|
||||||
|
BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
|
||||||
|
candidate,
|
||||||
|
attribute,
|
||||||
|
candidate.MethodSymbol.Name,
|
||||||
|
fieldSymbol.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol))
|
||||||
|
{
|
||||||
|
ReportMethodDiagnostic(
|
||||||
|
context,
|
||||||
|
BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
|
||||||
|
candidate,
|
||||||
|
attribute,
|
||||||
|
fieldSymbol.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventSymbol = FindEvent(fieldSymbol.Type, signalName);
|
||||||
|
if (eventSymbol is null)
|
||||||
|
{
|
||||||
|
ReportMethodDiagnostic(
|
||||||
|
context,
|
||||||
|
BindNodeSignalDiagnostics.SignalNotFound,
|
||||||
|
candidate,
|
||||||
|
attribute,
|
||||||
|
fieldSymbol.Name,
|
||||||
|
signalName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
|
||||||
|
{
|
||||||
|
ReportMethodDiagnostic(
|
||||||
|
context,
|
||||||
|
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
|
||||||
|
candidate,
|
||||||
|
attribute,
|
||||||
|
candidate.MethodSymbol.Name,
|
||||||
|
eventSymbol.Name,
|
||||||
|
fieldSymbol.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
binding = new SignalBindingInfo(fieldSymbol, eventSymbol, candidate.MethodSymbol);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportMethodDiagnostic(
|
||||||
|
SourceProductionContext context,
|
||||||
|
DiagnosticDescriptor descriptor,
|
||||||
|
MethodCandidate candidate,
|
||||||
|
AttributeData attribute,
|
||||||
|
params object[] messageArgs)
|
||||||
|
{
|
||||||
|
var location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ??
|
||||||
|
candidate.Method.Identifier.GetLocation();
|
||||||
|
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveCtorString(
|
||||||
|
AttributeData attribute,
|
||||||
|
int index)
|
||||||
|
{
|
||||||
|
if (attribute.ConstructorArguments.Length <= index)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return attribute.ConstructorArguments[index].Value as string ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<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 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user