GeWuYou be928718e3 feat: 增强 AutoSceneGenerator 与 AutoRegisterExportedCollectionsGenerator 的验证与安全机制
### AutoSceneGenerator
- 引入保留成员名称集合(GeneratedMemberNames),包含:
  - SceneKeyStr
  - __autoSceneBehavior_Generated
- 实现 ReportGeneratedMemberConflicts 方法:
  - 检测用户定义成员与生成成员冲突
  - 提供清晰的诊断信息
- 在生成流程中集成冲突检测,避免重复成员导致的编译错误

### AutoRegisterExportedCollectionsGenerator
- 增强集合注册生成器的验证逻辑:
  - 新增诊断 GF_AutoExport_006:导出集合成员必须为实例可读成员
  - 新增诊断 GF_AutoExport_007:注册表成员必须为实例可读成员
- 实现 IsInstanceReadableMember 方法:
  - 校验成员为非静态字段或可读属性
- 修复符号访问性检查:
  - 确保注册方法对所有者类型可访问
- 优化生成逻辑:
  - 过滤重复的部分类声明,仅生成一次源码

### Tests
- AutoSceneGenerator
  - 覆盖保留成员冲突场景:
    - SceneKeyStr 冲突
    - __autoSceneBehavior_Generated 冲突

- AutoRegisterExportedCollectionsGenerator
  - 覆盖完整验证逻辑:
    - 不可读成员 → GF_AutoExport_006 / 007
    - 方法不可访问 → GF_AutoExport_003
    - 多个 partial class → 仅生成一个源文件
2026-04-13 13:04:00 +08:00

332 lines
14 KiB
C#

using GFramework.Godot.SourceGenerators.Behavior;
using GFramework.Godot.SourceGenerators.Tests.Core;
namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture]
public class AutoSceneGeneratorTests
{
[Test]
public async Task Generates_Scene_Behavior_Boilerplate()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;
public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class GameplayRoot
{
private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated;
public static string SceneKeyStr => "Gameplay";
public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene()
{
return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr);
}
}
""";
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute() { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace TestApp
{
[{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoBehavior_004", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
await test.RunAsync();
}
[Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{
const string source = """
#nullable enable
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;
public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated;
public static string SceneKeyStr => "Gameplay";
public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene()
{
return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr);
}
}
""";
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
/// <summary>
/// 验证宿主类型声明同名 <c>SceneKeyStr</c> 属性时,生成器会报告保留成员冲突并停止生成。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_SceneKeyStr_Property_Name_Conflicts()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
public static string {|#0:SceneKeyStr|} => "Conflict";
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, 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("GameplayRoot", "SceneKeyStr"));
await test.RunAsync();
}
/// <summary>
/// 验证宿主类型声明同名缓存字段时,生成器会报告保留成员冲突并停止生成。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Generated_Behavior_Field_Name_Conflicts()
{
const string source = """
using System;
using GFramework.Game.Abstractions.Scene;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
private ISceneBehavior? {|#0:__autoSceneBehavior_Generated|};
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, 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("GameplayRoot", "__autoSceneBehavior_Generated"));
await test.RunAsync();
}
}