GeWuYou 6fa4580893 feat(generator): 添加 BindNodeSignal 和 GetNode 源代码生成器
- 实现 BindNodeSignalGenerator 用于生成节点信号绑定与解绑逻辑
- 实现 GetNodeGenerator 用于生成 Godot 节点获取注入逻辑
- 添加 BindNodeSignalDiagnostics 提供详细的诊断错误信息
- 集成到 AnalyzerReleases.Unshipped.md 追踪新的分析规则
- 支持 [BindNodeSignal] 属性的方法自动生成事件绑定代码
- 支持 [GetNode] 属性的字段自动生成节点获取代码
- 提供生命周期方法集成的智能提示和验证功能
2026-03-31 11:11:23 +08:00

306 lines
13 KiB
C#

using GFramework.Godot.SourceGenerators.Tests.Core;
namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
[TestFixture]
public class GetNodeGeneratorTests
{
[Test]
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{
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 GetNodeAttribute(string path) { Path = path; }
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
}
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!;
[GetNode]
private HBoxContainer m_rightContainer = null!;
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class TopBar
{
private void __InjectGetNodes_Generated()
{
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
m_rightContainer = GetNode<global::Godot.HBoxContainer>("%RightContainer");
}
partial void OnGetNodeReadyGenerated();
public override void _Ready()
{
__InjectGetNodes_Generated();
OnGetNodeReadyGenerated();
}
}
""";
await GeneratorTest<GetNodeGenerator>.RunAsync(
source,
("TestApp_TopBar.GetNode.g.cs", expected));
}
[Test]
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
{
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 GetNodeAttribute(string path) { Path = path; }
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
}
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("%LeftContainer")]
private HBoxContainer _leftContainer = null!;
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
private HBoxContainer? _rightContainer;
public override void _Ready()
{
__InjectGetNodes_Generated();
}
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class TopBar
{
private void __InjectGetNodes_Generated()
{
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
_rightContainer = GetNodeOrNull<global::Godot.HBoxContainer>("RightContainer");
}
}
""";
await GeneratorTest<GetNodeGenerator>.RunAsync(
source,
("TestApp_TopBar.GetNode.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_FieldType_IsNotGodotNode()
{
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 string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
}
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;
}
}
namespace TestApp
{
public partial class TopBar : Node
{
[GetNode]
private string _leftContainer = string.Empty;
}
}
""";
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_GetNode_004", DiagnosticSeverity.Error)
.WithSpan(39, 24, 39, 38)
.WithArguments("_leftContainer"));
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();
}
}