mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-14 06:34:30 +08:00
Compare commits
No commits in common. "59e0c4ea684e90ddcd7ac1b3633bf91f7f6439b1" and "83cceed57b35bf4cebf275f80ceb51db67b3975a" have entirely different histories.
59e0c4ea68
...
83cceed57b
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,8 +5,6 @@ riderModule.iml
|
|||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
GFramework.sln.DotSettings.user
|
GFramework.sln.DotSettings.user
|
||||||
.idea/
|
.idea/
|
||||||
dotnet-home/
|
|
||||||
scripts/__pycache__/
|
|
||||||
# ai
|
# ai
|
||||||
opencode.json
|
opencode.json
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标记类型允许为带映射特性的导出集合生成批量注册代码。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标记场景根节点类型,Source Generator 会生成场景行为样板代码。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoSceneAttribute(string key) : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取场景键。
|
|
||||||
/// </summary>
|
|
||||||
public string Key { get; } = key;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标记 UI 页面类型,Source Generator 会生成页面行为样板代码。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoUiPageAttribute(string key, string layerName) : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 UI 键。
|
|
||||||
/// </summary>
|
|
||||||
public string Key { get; } = key;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 <c>UiLayer</c> 枚举成员名称。
|
|
||||||
/// </summary>
|
|
||||||
public string LayerName { get; } = layerName;
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 声明导出集合应当转发到哪个注册器成员及其方法。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName)
|
|
||||||
: Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取注册器字段或属性名称。
|
|
||||||
/// </summary>
|
|
||||||
public string RegistryMemberName { get; } = registryMemberName;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取注册方法名称。
|
|
||||||
/// </summary>
|
|
||||||
public string RegisterMethodName { get; } = registerMethodName;
|
|
||||||
}
|
|
||||||
@ -1,331 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,273 +0,0 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Behavior;
|
|
||||||
using GFramework.Godot.SourceGenerators.Tests.Core;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class AutoUiPageGeneratorTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Ui_Page_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 AutoUiPageAttribute : Attribute
|
|
||||||
{
|
|
||||||
public AutoUiPageAttribute(string key, string layerName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class CanvasItem : Node { }
|
|
||||||
public class Control : CanvasItem { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Enums
|
|
||||||
{
|
|
||||||
public enum UiLayer
|
|
||||||
{
|
|
||||||
Page,
|
|
||||||
Overlay,
|
|
||||||
Modal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI
|
|
||||||
{
|
|
||||||
public interface IUiPageBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.UI
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class UiPageBehaviorFactory
|
|
||||||
{
|
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
|
||||||
where T : CanvasItem
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[AutoUiPage("MainMenu", "Page")]
|
|
||||||
public partial class MainMenu : Control
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class MainMenu
|
|
||||||
{
|
|
||||||
private global::GFramework.Game.Abstractions.UI.IUiPageBehavior? __autoUiPageBehavior_Generated;
|
|
||||||
|
|
||||||
public static string UiKeyStr => "MainMenu";
|
|
||||||
|
|
||||||
public global::GFramework.Game.Abstractions.UI.IUiPageBehavior GetPage()
|
|
||||||
{
|
|
||||||
return __autoUiPageBehavior_Generated ??= global::GFramework.Godot.UI.UiPageBehaviorFactory.Create(this, UiKeyStr, global::GFramework.Game.Abstractions.Enums.UiLayer.Page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_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 AutoUiPageAttribute : Attribute
|
|
||||||
{
|
|
||||||
public AutoUiPageAttribute(string key) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class CanvasItem : Node { }
|
|
||||||
public class Control : CanvasItem { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Enums
|
|
||||||
{
|
|
||||||
public enum UiLayer
|
|
||||||
{
|
|
||||||
Page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI
|
|
||||||
{
|
|
||||||
public interface IUiPageBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.UI
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class UiPageBehaviorFactory
|
|
||||||
{
|
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
|
||||||
where T : CanvasItem
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[{|#0:AutoUiPage("MainMenu")|}]
|
|
||||||
public partial class MainMenu : Control
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoBehavior_004", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments(
|
|
||||||
"AutoUiPageAttribute",
|
|
||||||
"MainMenu",
|
|
||||||
"a string key argument and a string UiLayer name argument"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
|
|
||||||
{
|
|
||||||
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 AutoUiPageAttribute : Attribute
|
|
||||||
{
|
|
||||||
public AutoUiPageAttribute(string key, string layerName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class CanvasItem : Node { }
|
|
||||||
public class Control : CanvasItem { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Enums
|
|
||||||
{
|
|
||||||
public enum UiLayer
|
|
||||||
{
|
|
||||||
Page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI
|
|
||||||
{
|
|
||||||
public interface IUiPageBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.UI
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class UiPageBehaviorFactory
|
|
||||||
{
|
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
|
||||||
where T : CanvasItem
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[AutoUiPage("MainMenu", "Page")]
|
|
||||||
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class MainMenu<TReference, TNotNull, TUnmanaged>
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
private global::GFramework.Game.Abstractions.UI.IUiPageBehavior? __autoUiPageBehavior_Generated;
|
|
||||||
|
|
||||||
public static string UiKeyStr => "MainMenu";
|
|
||||||
|
|
||||||
public global::GFramework.Game.Abstractions.UI.IUiPageBehavior GetPage()
|
|
||||||
{
|
|
||||||
return __autoUiPageBehavior_Generated ??= global::GFramework.Godot.UI.UiPageBehaviorFactory.Create(this, UiKeyStr, global::GFramework.Game.Abstractions.Enums.UiLayer.Page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,780 +0,0 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Registration;
|
|
||||||
using GFramework.Godot.SourceGenerators.Tests.Core;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Tests.Registration;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class AutoRegisterExportedCollectionsGeneratorTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TValue : struct
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
private readonly IntRegistry? _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TValue : struct
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
private void __RegisterExportedCollections_Generated()
|
|
||||||
{
|
|
||||||
if (this.Values is not null && this._registry is not null)
|
|
||||||
{
|
|
||||||
foreach (var __generatedItem in this.Values)
|
|
||||||
{
|
|
||||||
this._registry.Register(__generatedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_Collection_Element_Type_Cannot_Be_Inferred()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IntRegistry _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public IEnumerable {|#0:Values|} { get; } = new ArrayList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_005", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("Values"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class ArrayRegistry
|
|
||||||
{
|
|
||||||
public void Register(int[] value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly ArrayRegistry _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
|
|
||||||
public List<int[]> Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private void __RegisterExportedCollections_Generated()
|
|
||||||
{
|
|
||||||
if (this.Values is not null && this._registry is not null)
|
|
||||||
{
|
|
||||||
foreach (var __generatedItem in this.Values)
|
|
||||||
{
|
|
||||||
this._registry.Register(__generatedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public interface IKeyValue<TKey, TValue>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IRegistry<TKey, TValue>
|
|
||||||
{
|
|
||||||
void Registry(IKeyValue<TKey, TValue> mapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class IntConfig : IKeyValue<string, int>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IAssetRegistry<int>? _registry = null;
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), "Registry")]
|
|
||||||
public List<IntConfig>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private void __RegisterExportedCollections_Generated()
|
|
||||||
{
|
|
||||||
if (this.Values is not null && this._registry is not null)
|
|
||||||
{
|
|
||||||
foreach (var __generatedItem in this.Values)
|
|
||||||
{
|
|
||||||
this._registry.Registry(__generatedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_Register_Method_Is_Only_Explicitly_Implemented_Interface_Member()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public interface IRegistry
|
|
||||||
{
|
|
||||||
void Register(int value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ExplicitRegistry : IRegistry
|
|
||||||
{
|
|
||||||
void IRegistry.Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly ExplicitRegistry _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), "Register")]
|
|
||||||
public List<int> {|#0:Values|} { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("Register", "_registry", "Values"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public class BaseRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class DerivedRegistry : BaseRegistry
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly DerivedRegistry? _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private void __RegisterExportedCollections_Generated()
|
|
||||||
{
|
|
||||||
if (this.Values is not null && this._registry is not null)
|
|
||||||
{
|
|
||||||
foreach (var __generatedItem in this.Values)
|
|
||||||
{
|
|
||||||
this._registry.Register(__generatedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class BootstrapperBase
|
|
||||||
{
|
|
||||||
protected readonly IntRegistry? _registry = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper : BootstrapperBase
|
|
||||||
{
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private void __RegisterExportedCollections_Generated()
|
|
||||||
{
|
|
||||||
if (this.Values is not null && this._registry is not null)
|
|
||||||
{
|
|
||||||
foreach (var __generatedItem in this.Values)
|
|
||||||
{
|
|
||||||
this._registry.Register(__generatedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IntRegistry _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public static List<int> {|#0:StaticValues|} = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int> {|#2:WriteOnlyValues|} { set { } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("StaticValues"));
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(1)
|
|
||||||
.WithArguments("StaticPropertyValues"));
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(2)
|
|
||||||
.WithArguments("WriteOnlyValues"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_Registry_Member_Is_Not_Instance_Readable()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private static readonly IntRegistry {|#0:_registry|} = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int> Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_007", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("_registry", "Values"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_Register_Method_Is_Not_Accessible_From_Owner_Type()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
private void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IntRegistry _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), "Register")]
|
|
||||||
public List<int> {|#0:Values|} { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("Register", "_registry", "Values"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_RegisterExportedCollection_Attribute_Arguments_Are_Invalid()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IntRegistry _registry = new();
|
|
||||||
|
|
||||||
[{|#0:RegisterExportedCollection(nameof(_registry))|}]
|
|
||||||
public List<int> Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_008", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("Values"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IntRegistry? _registry = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private void __RegisterExportedCollections_Generated()
|
|
||||||
{
|
|
||||||
if (this.Values is not null && this._registry is not null)
|
|
||||||
{
|
|
||||||
foreach (var __generatedItem in this.Values)
|
|
||||||
{
|
|
||||||
this._registry.Register(__generatedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
### 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
|
||||||
@ -21,15 +21,3 @@
|
|||||||
GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_AutoBehavior_001 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
|
||||||
GF_AutoBehavior_002 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
|
||||||
GF_AutoBehavior_003 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
|
||||||
GF_AutoBehavior_004 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
|
||||||
GF_AutoExport_001 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_002 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_003 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_004 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_005 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_006 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_007 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
GF_AutoExport_008 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
|
|||||||
@ -1,339 +0,0 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Behavior;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为标记了 <c>[AutoScene]</c> 的 Godot 节点生成场景行为样板。
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// 该生成器会为兼容的非嵌套 <c>partial</c> Godot 节点类型生成 <c>SceneKeyStr</c> 与 <c>GetScene</c>,
|
|
||||||
/// 以便通过 <c>SceneBehaviorFactory</c> 延迟创建并缓存场景行为实例。
|
|
||||||
/// 生成管线仅处理显式标记了 <c>AutoSceneAttribute</c> 的类,并在类型不满足基类、<c>partial</c>、
|
|
||||||
/// 成员冲突或属性参数约束时通过诊断停止生成,而不是静默回退到不完整输出。
|
|
||||||
/// </remarks>
|
|
||||||
[Generator]
|
|
||||||
public sealed class AutoSceneGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
private const string AutoSceneAttributeMetadataName =
|
|
||||||
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute";
|
|
||||||
private static readonly string[] GeneratedMemberNames =
|
|
||||||
[
|
|
||||||
"SceneKeyStr",
|
|
||||||
"__autoSceneBehavior_Generated"
|
|
||||||
];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 配置 <c>AutoScene</c> 的增量生成管线。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">用于注册语法筛选、语义转换和源输出阶段的增量生成上下文。</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// 管线首先通过语法节点名称快速筛选潜在候选,再结合语义模型确认类型符号。
|
|
||||||
/// 最终输出阶段仅在 <c>AutoSceneAttribute</c>、<c>Godot.Node</c> 等依赖可解析且目标类型满足生成约束时产出源码;
|
|
||||||
/// 否则会报告对应诊断,或在宿主依赖缺失时直接跳过生成。
|
|
||||||
/// </remarks>
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
|
|
||||||
static (node, _) => IsCandidate(node),
|
|
||||||
static (syntaxContext, _) => Transform(syntaxContext))
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
return node is ClassDeclarationSyntax classDeclaration &&
|
|
||||||
classDeclaration.AttributeLists
|
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute => attribute.Name.ToString().Contains("AutoScene", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TypeCandidate? Transform(GeneratorSyntaxContext context)
|
|
||||||
{
|
|
||||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new TypeCandidate(classDeclaration, typeSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Execute(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
ImmutableArray<TypeCandidate?> candidates)
|
|
||||||
{
|
|
||||||
if (candidates.IsDefaultOrEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var autoSceneAttribute = compilation.GetTypeByMetadataName(AutoSceneAttributeMetadataName);
|
|
||||||
var godotNodeType = compilation.GetTypeByMetadataName("Godot.Node");
|
|
||||||
|
|
||||||
if (autoSceneAttribute is null || godotNodeType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var candidate in candidates.Where(static candidate => candidate is not null)
|
|
||||||
.Select(static candidate => candidate!))
|
|
||||||
{
|
|
||||||
var attribute = candidate.TypeSymbol.GetAttributes()
|
|
||||||
.FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, autoSceneAttribute));
|
|
||||||
|
|
||||||
if (attribute is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!CanGenerateForType(context, candidate, godotNodeType))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (candidate.TypeSymbol.ReportGeneratedMethodConflicts(
|
|
||||||
context,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
"GetScene"))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReportGeneratedMemberConflicts(
|
|
||||||
context,
|
|
||||||
candidate.TypeSymbol,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
GeneratedMemberNames))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryGetSceneKey(context, candidate.TypeSymbol, attribute, out var key))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanGenerateForType(
|
|
||||||
SourceProductionContext context,
|
|
||||||
TypeCandidate candidate,
|
|
||||||
INamedTypeSymbol requiredBaseType)
|
|
||||||
{
|
|
||||||
if (candidate.TypeSymbol.ContainingType is not null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.NestedClassNotSupported,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
"AutoScene",
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsPartial(candidate.TypeSymbol))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
CommonDiagnostics.ClassMustBePartial,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.TypeSymbol.IsAssignableTo(requiredBaseType))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.MissingBaseType,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name,
|
|
||||||
requiredBaseType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
|
|
||||||
"AutoScene"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetSceneKey(
|
|
||||||
SourceProductionContext context,
|
|
||||||
INamedTypeSymbol typeSymbol,
|
|
||||||
AttributeData attribute,
|
|
||||||
out string key)
|
|
||||||
{
|
|
||||||
key = string.Empty;
|
|
||||||
|
|
||||||
if (attribute.ConstructorArguments.Length == 1 &&
|
|
||||||
attribute.ConstructorArguments[0].Value is string sceneKey)
|
|
||||||
{
|
|
||||||
key = sceneKey;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.InvalidAttributeArguments,
|
|
||||||
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ??
|
|
||||||
typeSymbol.Locations.FirstOrDefault() ??
|
|
||||||
Location.None,
|
|
||||||
"AutoSceneAttribute",
|
|
||||||
typeSymbol.Name,
|
|
||||||
"a single string scene key argument"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenerateSource(INamedTypeSymbol typeSymbol, string key)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("// <auto-generated />");
|
|
||||||
builder.AppendLine("#nullable enable");
|
|
||||||
builder.AppendLine();
|
|
||||||
|
|
||||||
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? null
|
|
||||||
: typeSymbol.ContainingNamespace.ToDisplayString();
|
|
||||||
|
|
||||||
if (ns is not null)
|
|
||||||
{
|
|
||||||
builder.AppendLine($"namespace {ns};");
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AppendLine($"{GetTypeDeclarationKeyword(typeSymbol)} {GetTypeDeclarationName(typeSymbol)}");
|
|
||||||
AppendTypeConstraints(builder, typeSymbol);
|
|
||||||
builder.AppendLine("{");
|
|
||||||
builder.AppendLine(
|
|
||||||
" private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated;");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.Append(" public static string SceneKeyStr => ");
|
|
||||||
builder.Append(SymbolDisplay.FormatLiteral(key, true));
|
|
||||||
builder.AppendLine(";");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.AppendLine(" public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene()");
|
|
||||||
builder.AppendLine(" {");
|
|
||||||
builder.AppendLine(
|
|
||||||
" return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr);");
|
|
||||||
builder.AppendLine(" }");
|
|
||||||
builder.AppendLine("}");
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPartial(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.DeclaringSyntaxReferences
|
|
||||||
.Select(static reference => reference.GetSyntax())
|
|
||||||
.OfType<ClassDeclarationSyntax>()
|
|
||||||
.All(static declaration =>
|
|
||||||
declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetHintName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
var prefix = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? typeSymbol.Name
|
|
||||||
: $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
|
|
||||||
return prefix.Replace('.', '_') + ".AutoScene.g.cs";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.IsRecord
|
|
||||||
? typeSymbol.TypeKind == TypeKind.Struct ? "partial record struct" : "partial record"
|
|
||||||
: typeSymbol.TypeKind == TypeKind.Struct
|
|
||||||
? "partial struct"
|
|
||||||
: "partial class";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
if (typeSymbol.TypeParameters.Length == 0)
|
|
||||||
return typeSymbol.Name;
|
|
||||||
|
|
||||||
return
|
|
||||||
$"{typeSymbol.Name}<{string.Join(", ", typeSymbol.TypeParameters.Select(static parameter => parameter.Name))}>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendTypeConstraints(StringBuilder builder, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
foreach (var typeParameter in typeSymbol.TypeParameters)
|
|
||||||
{
|
|
||||||
var constraints = new List<string>();
|
|
||||||
|
|
||||||
if (typeParameter.HasReferenceTypeConstraint)
|
|
||||||
{
|
|
||||||
constraints.Add(
|
|
||||||
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
|
|
||||||
? "class?"
|
|
||||||
: "class");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeParameter.HasNotNullConstraint)
|
|
||||||
constraints.Add("notnull");
|
|
||||||
|
|
||||||
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
|
|
||||||
if (typeParameter.HasUnmanagedTypeConstraint)
|
|
||||||
constraints.Add("unmanaged");
|
|
||||||
else if (typeParameter.HasValueTypeConstraint)
|
|
||||||
constraints.Add("struct");
|
|
||||||
|
|
||||||
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>
|
|
||||||
constraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
|
|
||||||
|
|
||||||
if (typeParameter.HasConstructorConstraint)
|
|
||||||
constraints.Add("new()");
|
|
||||||
|
|
||||||
if (constraints.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
builder.Append(" where ");
|
|
||||||
builder.Append(typeParameter.Name);
|
|
||||||
builder.Append(" : ");
|
|
||||||
builder.AppendLine(string.Join(", ", constraints));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告与生成器保留成员名冲突的字段或属性,避免生成代码出现重复成员编译错误。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">用于上报诊断的源代码生成上下文。</param>
|
|
||||||
/// <param name="typeSymbol">当前待生成的类型符号。</param>
|
|
||||||
/// <param name="fallbackLocation">冲突成员无定位信息时的后备位置。</param>
|
|
||||||
/// <param name="memberNames">需要校验的生成器保留成员名集合。</param>
|
|
||||||
/// <returns>存在任意冲突时返回 <c>true</c>。</returns>
|
|
||||||
private static bool ReportGeneratedMemberConflicts(
|
|
||||||
SourceProductionContext context,
|
|
||||||
INamedTypeSymbol typeSymbol,
|
|
||||||
Location fallbackLocation,
|
|
||||||
string[] memberNames)
|
|
||||||
{
|
|
||||||
var hasConflict = false;
|
|
||||||
|
|
||||||
foreach (var memberName in memberNames)
|
|
||||||
{
|
|
||||||
var conflict = typeSymbol.GetMembers(memberName)
|
|
||||||
.FirstOrDefault(member =>
|
|
||||||
!member.IsImplicitlyDeclared &&
|
|
||||||
member is IPropertySymbol or IFieldSymbol);
|
|
||||||
|
|
||||||
if (conflict is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
CommonDiagnostics.GeneratedMethodNameConflict,
|
|
||||||
conflict.Locations.FirstOrDefault() ?? fallbackLocation,
|
|
||||||
typeSymbol.Name,
|
|
||||||
memberName));
|
|
||||||
hasConflict = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasConflict;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TypeCandidate
|
|
||||||
{
|
|
||||||
public TypeCandidate(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
ClassDeclaration = classDeclaration;
|
|
||||||
TypeSymbol = typeSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassDeclarationSyntax ClassDeclaration { get; }
|
|
||||||
|
|
||||||
public INamedTypeSymbol TypeSymbol { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,301 +0,0 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Behavior;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为标记了 <c>[AutoUiPage]</c> 的 Godot CanvasItem 生成页面行为样板。
|
|
||||||
/// </summary>
|
|
||||||
[Generator]
|
|
||||||
public sealed class AutoUiPageGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
private const string AutoUiPageAttributeMetadataName =
|
|
||||||
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoUiPageAttribute";
|
|
||||||
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
|
|
||||||
static (node, _) => IsCandidate(node),
|
|
||||||
static (syntaxContext, _) => Transform(syntaxContext))
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
return node is ClassDeclarationSyntax classDeclaration &&
|
|
||||||
classDeclaration.AttributeLists
|
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute => attribute.Name.ToString().Contains("AutoUiPage", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TypeCandidate? Transform(GeneratorSyntaxContext context)
|
|
||||||
{
|
|
||||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new TypeCandidate(classDeclaration, typeSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Execute(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
ImmutableArray<TypeCandidate?> candidates)
|
|
||||||
{
|
|
||||||
if (candidates.IsDefaultOrEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var autoUiPageAttribute = compilation.GetTypeByMetadataName(AutoUiPageAttributeMetadataName);
|
|
||||||
var canvasItemType = compilation.GetTypeByMetadataName("Godot.CanvasItem");
|
|
||||||
var uiLayerType = compilation.GetTypeByMetadataName("GFramework.Game.Abstractions.Enums.UiLayer");
|
|
||||||
|
|
||||||
if (autoUiPageAttribute is null || canvasItemType is null || uiLayerType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var candidate in candidates.Where(static candidate => candidate is not null)
|
|
||||||
.Select(static candidate => candidate!))
|
|
||||||
{
|
|
||||||
var attribute = candidate.TypeSymbol.GetAttributes()
|
|
||||||
.FirstOrDefault(attr =>
|
|
||||||
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, autoUiPageAttribute));
|
|
||||||
|
|
||||||
if (attribute is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!CanGenerateForType(context, candidate, canvasItemType, "AutoUiPage"))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (candidate.TypeSymbol.ReportGeneratedMethodConflicts(
|
|
||||||
context,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
"GetPage"))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryCreateSpec(context, candidate.TypeSymbol, attribute, uiLayerType, out var spec))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, spec));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanGenerateForType(
|
|
||||||
SourceProductionContext context,
|
|
||||||
TypeCandidate candidate,
|
|
||||||
INamedTypeSymbol requiredBaseType,
|
|
||||||
string generatorName)
|
|
||||||
{
|
|
||||||
if (candidate.TypeSymbol.ContainingType is not null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.NestedClassNotSupported,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
generatorName,
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsPartial(candidate.TypeSymbol))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
CommonDiagnostics.ClassMustBePartial,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.TypeSymbol.IsAssignableTo(requiredBaseType))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.MissingBaseType,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name,
|
|
||||||
requiredBaseType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
|
|
||||||
generatorName));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryCreateSpec(
|
|
||||||
SourceProductionContext context,
|
|
||||||
INamedTypeSymbol typeSymbol,
|
|
||||||
AttributeData attribute,
|
|
||||||
INamedTypeSymbol uiLayerType,
|
|
||||||
out UiPageSpec spec)
|
|
||||||
{
|
|
||||||
spec = null!;
|
|
||||||
|
|
||||||
if (attribute.ConstructorArguments.Length != 2 ||
|
|
||||||
attribute.ConstructorArguments[0].Value is not string key ||
|
|
||||||
attribute.ConstructorArguments[1].Value is not string layerName)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.InvalidAttributeArguments,
|
|
||||||
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation()
|
|
||||||
?? typeSymbol.Locations.FirstOrDefault()
|
|
||||||
?? Location.None,
|
|
||||||
"AutoUiPageAttribute",
|
|
||||||
typeSymbol.Name,
|
|
||||||
"a string key argument and a string UiLayer name argument"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uiLayerType.GetMembers(layerName).Any())
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoBehaviorDiagnostics.InvalidUiLayerName,
|
|
||||||
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? Location.None,
|
|
||||||
layerName,
|
|
||||||
typeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
spec = new UiPageSpec(key, layerName);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenerateSource(INamedTypeSymbol typeSymbol, UiPageSpec spec)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("// <auto-generated />");
|
|
||||||
builder.AppendLine("#nullable enable");
|
|
||||||
builder.AppendLine();
|
|
||||||
|
|
||||||
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? null
|
|
||||||
: typeSymbol.ContainingNamespace.ToDisplayString();
|
|
||||||
|
|
||||||
if (ns is not null)
|
|
||||||
{
|
|
||||||
builder.AppendLine($"namespace {ns};");
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AppendLine($"{GetTypeDeclarationKeyword(typeSymbol)} {GetTypeDeclarationName(typeSymbol)}");
|
|
||||||
AppendTypeConstraints(builder, typeSymbol);
|
|
||||||
builder.AppendLine("{");
|
|
||||||
builder.AppendLine(
|
|
||||||
" private global::GFramework.Game.Abstractions.UI.IUiPageBehavior? __autoUiPageBehavior_Generated;");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.Append(" public static string UiKeyStr => ");
|
|
||||||
builder.Append(SymbolDisplay.FormatLiteral(spec.Key, true));
|
|
||||||
builder.AppendLine(";");
|
|
||||||
builder.AppendLine();
|
|
||||||
builder.AppendLine(" public global::GFramework.Game.Abstractions.UI.IUiPageBehavior GetPage()");
|
|
||||||
builder.AppendLine(" {");
|
|
||||||
builder.AppendLine(
|
|
||||||
$" return __autoUiPageBehavior_Generated ??= global::GFramework.Godot.UI.UiPageBehaviorFactory.Create(this, UiKeyStr, global::GFramework.Game.Abstractions.Enums.UiLayer.{spec.LayerName});");
|
|
||||||
builder.AppendLine(" }");
|
|
||||||
builder.AppendLine("}");
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPartial(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.DeclaringSyntaxReferences
|
|
||||||
.Select(static reference => reference.GetSyntax())
|
|
||||||
.OfType<ClassDeclarationSyntax>()
|
|
||||||
.All(static declaration =>
|
|
||||||
declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetHintName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
var prefix = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? typeSymbol.Name
|
|
||||||
: $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
|
|
||||||
return prefix.Replace('.', '_') + ".AutoUiPage.g.cs";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.IsRecord
|
|
||||||
? typeSymbol.TypeKind == TypeKind.Struct ? "partial record struct" : "partial record"
|
|
||||||
: typeSymbol.TypeKind == TypeKind.Struct
|
|
||||||
? "partial struct"
|
|
||||||
: "partial class";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
if (typeSymbol.TypeParameters.Length == 0)
|
|
||||||
return typeSymbol.Name;
|
|
||||||
|
|
||||||
return
|
|
||||||
$"{typeSymbol.Name}<{string.Join(", ", typeSymbol.TypeParameters.Select(static parameter => parameter.Name))}>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendTypeConstraints(StringBuilder builder, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
foreach (var typeParameter in typeSymbol.TypeParameters)
|
|
||||||
{
|
|
||||||
var constraints = new List<string>();
|
|
||||||
|
|
||||||
if (typeParameter.HasReferenceTypeConstraint)
|
|
||||||
{
|
|
||||||
constraints.Add(
|
|
||||||
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
|
|
||||||
? "class?"
|
|
||||||
: "class");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeParameter.HasNotNullConstraint)
|
|
||||||
constraints.Add("notnull");
|
|
||||||
|
|
||||||
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
|
|
||||||
if (typeParameter.HasUnmanagedTypeConstraint)
|
|
||||||
constraints.Add("unmanaged");
|
|
||||||
else if (typeParameter.HasValueTypeConstraint)
|
|
||||||
constraints.Add("struct");
|
|
||||||
|
|
||||||
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>
|
|
||||||
constraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
|
|
||||||
|
|
||||||
if (typeParameter.HasConstructorConstraint)
|
|
||||||
constraints.Add("new()");
|
|
||||||
|
|
||||||
if (constraints.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
builder.Append(" where ");
|
|
||||||
builder.Append(typeParameter.Name);
|
|
||||||
builder.Append(" : ");
|
|
||||||
builder.AppendLine(string.Join(", ", constraints));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TypeCandidate
|
|
||||||
{
|
|
||||||
public TypeCandidate(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
ClassDeclaration = classDeclaration;
|
|
||||||
TypeSymbol = typeSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassDeclarationSyntax ClassDeclaration { get; }
|
|
||||||
|
|
||||||
public INamedTypeSymbol TypeSymbol { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class UiPageSpec
|
|
||||||
{
|
|
||||||
public UiPageSpec(string key, string layerName)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
LayerName = layerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Key { get; }
|
|
||||||
|
|
||||||
public string LayerName { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义行为类自动生成器使用的诊断描述符。
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// 这些规则覆盖 <c>AutoScene</c> 与 <c>AutoUiPage</c> 等行为生成器的常见使用约束,
|
|
||||||
/// 以便在生成被跳过前向调用方报告明确的失败原因。
|
|
||||||
/// </remarks>
|
|
||||||
internal static class AutoBehaviorDiagnostics
|
|
||||||
{
|
|
||||||
private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Behavior";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告行为生成器不支持在嵌套类型上运行。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
|
||||||
"GF_AutoBehavior_001",
|
|
||||||
"Auto behavior generators do not support nested classes",
|
|
||||||
"Generator '{0}' does not support nested class '{1}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告目标类型没有继承生成器要求的 Godot 基类。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor MissingBaseType = new(
|
|
||||||
"GF_AutoBehavior_002",
|
|
||||||
"Auto behavior generators require a compatible base type",
|
|
||||||
"Type '{0}' must inherit from '{1}' to use '{2}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告 UI 页面声明中使用了不存在的 <c>UiLayer</c> 名称。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor InvalidUiLayerName = new(
|
|
||||||
"GF_AutoBehavior_003",
|
|
||||||
"Unknown UiLayer name",
|
|
||||||
"Ui layer '{0}' on '{1}' does not exist on GFramework.Game.Abstractions.Enums.UiLayer",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告行为生成器特性参数不满足约定签名,导致生成器无法推导所需元数据。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor InvalidAttributeArguments = new(
|
|
||||||
"GF_AutoBehavior_004",
|
|
||||||
"Auto behavior attribute arguments are invalid",
|
|
||||||
"Attribute '{0}' on '{1}' must provide {2}",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义导出集合自动注册生成器使用的诊断描述符。
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// 这些规则用于在源生成阶段验证集合成员、注册目标以及元素类型推导,
|
|
||||||
/// 避免把配置错误延后到生成代码编译或运行时才暴露。
|
|
||||||
/// </remarks>
|
|
||||||
internal static class AutoRegisterExportedCollectionsDiagnostics
|
|
||||||
{
|
|
||||||
private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Registration";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告自动注册生成器不支持嵌套类型。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
|
||||||
"GF_AutoExport_001",
|
|
||||||
"AutoRegisterExportedCollections does not support nested classes",
|
|
||||||
"AutoRegisterExportedCollections does not support nested class '{0}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告特性引用的注册表成员在宿主类型上不存在。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor RegistryMemberNotFound = new(
|
|
||||||
"GF_AutoExport_002",
|
|
||||||
"Registry member was not found",
|
|
||||||
"Member '{0}' referenced by exported collection '{1}' was not found on '{2}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告注册表上未找到与集合元素类型兼容的注册方法。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor RegisterMethodNotFound = new(
|
|
||||||
"GF_AutoExport_003",
|
|
||||||
"Register method was not found",
|
|
||||||
"Method '{0}' was not found on registry member '{1}' for exported collection '{2}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告被标记成员不是可枚举集合,因此无法执行批量注册。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor CollectionTypeMustBeEnumerable = new(
|
|
||||||
"GF_AutoExport_004",
|
|
||||||
"Exported collection must be enumerable",
|
|
||||||
"Member '{0}' must be enumerable to use RegisterExportedCollection",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告集合元素类型无法在编译期推导,因此无法安全匹配注册方法。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor CollectionElementTypeCouldNotBeInferred = new(
|
|
||||||
"GF_AutoExport_005",
|
|
||||||
"Exported collection element type could not be inferred",
|
|
||||||
"Member '{0}' must expose a generic enumerable element type to use RegisterExportedCollection safely",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告被标记为导出集合的成员不是实例可读成员,因此无法生成 <c>this.<member></c> 访问代码。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor CollectionMemberMustBeInstanceReadable = new(
|
|
||||||
"GF_AutoExport_006",
|
|
||||||
"Exported collection member must be an instance readable member",
|
|
||||||
"Member '{0}' must be an instance field or readable non-indexer instance property to use RegisterExportedCollection",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告注册表成员不是实例可读成员,因此生成器无法安全读取并调用注册方法。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor RegistryMemberMustBeInstanceReadable = new(
|
|
||||||
"GF_AutoExport_007",
|
|
||||||
"Registry member must be an instance readable member",
|
|
||||||
"Registry member '{0}' referenced by exported collection '{1}' must be an instance field or readable non-indexer instance property",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 报告 <c>RegisterExportedCollectionAttribute</c> 构造参数不满足约定,导致无法解析注册目标成员与方法名。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor InvalidAttributeArguments = new(
|
|
||||||
"GF_AutoExport_008",
|
|
||||||
"RegisterExportedCollection attribute arguments are invalid",
|
|
||||||
"Attribute 'RegisterExportedCollectionAttribute' on member '{0}' must provide a string registry member name and a string register method name",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
@ -12,10 +12,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
global using System;
|
global using System;
|
||||||
global using System.Collections.Immutable;
|
|
||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Text;
|
|
||||||
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;
|
||||||
|
|||||||
@ -1,589 +0,0 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Registration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为导出集合生成批量注册样板方法。
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// 该生成器会扫描标记了 <c>AutoRegisterExportedCollectionsAttribute</c> 的 <c>partial</c> 类型,
|
|
||||||
/// 为其中使用 <c>RegisterExportedCollectionAttribute</c> 声明的集合成员生成集中注册方法。
|
|
||||||
/// 仅当集合可枚举、元素类型可推导、注册表成员存在且可找到兼容的实例注册方法时才会输出代码;
|
|
||||||
/// 否则通过 <c>GF_AutoExport_001</c> 到 <c>GF_AutoExport_008</c> 以及公共 <c>ClassMustBePartial</c> 诊断显式阻止生成。
|
|
||||||
/// </remarks>
|
|
||||||
[Generator]
|
|
||||||
public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
private const string AutoRegisterExportedCollectionsAttributeMetadataName =
|
|
||||||
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoRegisterExportedCollectionsAttribute";
|
|
||||||
|
|
||||||
private const string RegisterExportedCollectionAttributeMetadataName =
|
|
||||||
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.RegisterExportedCollectionAttribute";
|
|
||||||
|
|
||||||
private const string GeneratedMethodName = "__RegisterExportedCollections_Generated";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 配置导出集合自动注册的增量生成管线。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">用于注册候选筛选、语义转换和最终源输出的增量生成上下文。</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// 管线先通过语法名称筛选减少分析范围,再在输出阶段验证特性、集合形状、注册目标与方法签名。
|
|
||||||
/// 当依赖类型无法解析时,生成器不会报告噪声诊断而是直接跳过;当用户代码违反生成约束时,会报告明确诊断并停止该类型的生成。
|
|
||||||
/// </remarks>
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
|
|
||||||
static (node, _) => IsCandidate(node),
|
|
||||||
static (syntaxContext, _) => Transform(syntaxContext))
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
return node is ClassDeclarationSyntax classDeclaration &&
|
|
||||||
classDeclaration.AttributeLists
|
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute =>
|
|
||||||
attribute.Name.ToString().Contains("AutoRegisterExportedCollections", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TypeCandidate? Transform(GeneratorSyntaxContext context)
|
|
||||||
{
|
|
||||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new TypeCandidate(classDeclaration, typeSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Execute(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
ImmutableArray<TypeCandidate?> candidates)
|
|
||||||
{
|
|
||||||
if (candidates.IsDefaultOrEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var autoRegisterAttribute =
|
|
||||||
compilation.GetTypeByMetadataName(AutoRegisterExportedCollectionsAttributeMetadataName);
|
|
||||||
var registerCollectionAttribute =
|
|
||||||
compilation.GetTypeByMetadataName(RegisterExportedCollectionAttributeMetadataName);
|
|
||||||
var enumerableType = compilation.GetTypeByMetadataName("System.Collections.IEnumerable");
|
|
||||||
|
|
||||||
if (autoRegisterAttribute is null || registerCollectionAttribute is null || enumerableType is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var candidate in candidates
|
|
||||||
.Where(static candidate => candidate is not null)
|
|
||||||
.Select(static candidate => candidate!)
|
|
||||||
.GroupBy(static candidate => candidate.TypeSymbol, SymbolEqualityComparer.Default)
|
|
||||||
.Select(static group => group.First()))
|
|
||||||
{
|
|
||||||
if (!candidate.TypeSymbol.GetAttributes().Any(attribute =>
|
|
||||||
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, autoRegisterAttribute)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CanGenerateForType(context, candidate))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (candidate.TypeSymbol.ReportGeneratedMethodConflicts(
|
|
||||||
context,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
GeneratedMethodName))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var registrations = CollectRegistrations(
|
|
||||||
context,
|
|
||||||
compilation,
|
|
||||||
candidate.TypeSymbol,
|
|
||||||
registerCollectionAttribute,
|
|
||||||
enumerableType);
|
|
||||||
|
|
||||||
if (registrations.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, registrations));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanGenerateForType(SourceProductionContext context, TypeCandidate candidate)
|
|
||||||
{
|
|
||||||
if (candidate.TypeSymbol.ContainingType is not null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.NestedClassNotSupported,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsPartial(candidate.TypeSymbol))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
CommonDiagnostics.ClassMustBePartial,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<RegistrationSpec> CollectRegistrations(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
INamedTypeSymbol typeSymbol,
|
|
||||||
INamedTypeSymbol registerCollectionAttribute,
|
|
||||||
INamedTypeSymbol enumerableType)
|
|
||||||
{
|
|
||||||
var registrations = new List<RegistrationSpec>();
|
|
||||||
|
|
||||||
foreach (var member in typeSymbol.GetMembers())
|
|
||||||
{
|
|
||||||
if (member is not IFieldSymbol and not IPropertySymbol)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var attribute = member.GetAttributes()
|
|
||||||
.FirstOrDefault(attr =>
|
|
||||||
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, registerCollectionAttribute));
|
|
||||||
|
|
||||||
if (attribute is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!TryCreateRegistration(
|
|
||||||
context,
|
|
||||||
compilation,
|
|
||||||
typeSymbol,
|
|
||||||
member,
|
|
||||||
attribute,
|
|
||||||
enumerableType,
|
|
||||||
out var registration))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
registrations.Add(registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return registrations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryCreateRegistration(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
INamedTypeSymbol ownerType,
|
|
||||||
ISymbol collectionMember,
|
|
||||||
AttributeData attribute,
|
|
||||||
INamedTypeSymbol enumerableType,
|
|
||||||
out RegistrationSpec registration)
|
|
||||||
{
|
|
||||||
registration = null!;
|
|
||||||
|
|
||||||
if (!IsInstanceReadableMember(collectionMember))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.CollectionMemberMustBeInstanceReadable,
|
|
||||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
|
||||||
collectionMember.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var collectionType = collectionMember switch
|
|
||||||
{
|
|
||||||
IFieldSymbol field => field.Type,
|
|
||||||
IPropertySymbol property => property.Type,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (collectionType is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!collectionType.IsAssignableTo(enumerableType))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable,
|
|
||||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
|
||||||
collectionMember.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName,
|
|
||||||
out var registerMethodName))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var registryMember = FindRegistryMember(ownerType, registryMemberName);
|
|
||||||
|
|
||||||
if (registryMember is null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.RegistryMemberNotFound,
|
|
||||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
|
||||||
registryMemberName,
|
|
||||||
collectionMember.Name,
|
|
||||||
ownerType.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsInstanceReadableMember(registryMember) ||
|
|
||||||
!compilation.IsSymbolAccessibleWithin(registryMember, ownerType))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.RegistryMemberMustBeInstanceReadable,
|
|
||||||
registryMember.Locations.FirstOrDefault() ?? Location.None,
|
|
||||||
registryMemberName,
|
|
||||||
collectionMember.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var registryType = registryMember switch
|
|
||||||
{
|
|
||||||
IFieldSymbol field => field.Type as INamedTypeSymbol,
|
|
||||||
IPropertySymbol property => property.Type as INamedTypeSymbol,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (registryType is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var elementType = TryGetElementType(collectionType);
|
|
||||||
if (elementType is null)
|
|
||||||
{
|
|
||||||
// Non-generic IEnumerable exposes elements as object at compile time, which is not safe
|
|
||||||
// for validating or generating a strongly typed registry call.
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.CollectionElementTypeCouldNotBeInferred,
|
|
||||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
|
||||||
collectionMember.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName)
|
|
||||||
.Any(method =>
|
|
||||||
!method.IsStatic &&
|
|
||||||
method.Parameters.Length == 1 &&
|
|
||||||
compilation.IsSymbolAccessibleWithin(method, ownerType) &&
|
|
||||||
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
|
|
||||||
|
|
||||||
if (!hasCompatibleMethod)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
|
|
||||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
|
||||||
registerMethodName,
|
|
||||||
registryMemberName,
|
|
||||||
collectionMember.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsInstanceReadableMember(ISymbol member)
|
|
||||||
{
|
|
||||||
// Generated code always reads through `this.<member>`, so only instance fields and
|
|
||||||
// readable non-indexer instance properties are valid targets.
|
|
||||||
return member switch
|
|
||||||
{
|
|
||||||
IFieldSymbol field => !field.IsStatic,
|
|
||||||
IPropertySymbol property =>
|
|
||||||
!property.IsStatic &&
|
|
||||||
property.Parameters.Length == 0 &&
|
|
||||||
property.GetMethod is not null,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanAcceptElementType(
|
|
||||||
Compilation compilation,
|
|
||||||
ITypeSymbol elementType,
|
|
||||||
ITypeSymbol parameterType)
|
|
||||||
{
|
|
||||||
if (elementType.IsAssignableTo(parameterType as INamedTypeSymbol))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Fall back to Roslyn's conversion rules so arrays and other non-named types are
|
|
||||||
// validated the same way the generated invocation will be bound by the compiler.
|
|
||||||
return compilation.ClassifyConversion(elementType, parameterType).IsImplicit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ISymbol? FindRegistryMember(
|
|
||||||
INamedTypeSymbol ownerType,
|
|
||||||
string registryMemberName)
|
|
||||||
{
|
|
||||||
for (var currentType = ownerType; currentType is not null; currentType = currentType.BaseType)
|
|
||||||
{
|
|
||||||
// Search the owner hierarchy one level at a time so the generator follows the same
|
|
||||||
// name-hiding order as `this.<member>` in generated code.
|
|
||||||
var candidateMember = currentType.GetMembers(registryMemberName)
|
|
||||||
.FirstOrDefault(static member => member is IFieldSymbol or IPropertySymbol);
|
|
||||||
|
|
||||||
if (candidateMember is not null)
|
|
||||||
return candidateMember;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 枚举给定注册表类型上可能承载批量注册入口的候选实例方法。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="registryType">声明注册表成员的静态类型。</param>
|
|
||||||
/// <param name="registerMethodName">特性参数中声明的注册方法名称。</param>
|
|
||||||
/// <returns>
|
|
||||||
/// 按“当前类型 -> 基类链 -> 接口继承链(仅当静态类型本身是接口)”顺序返回所有同名方法,
|
|
||||||
/// 供后续签名和可访问性筛选使用。
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 生成器需要沿当前类型和基类链查找方法,因为用户代码可能通过派生类字段引用基类实现;
|
|
||||||
/// 当注册表成员本身声明为接口类型时,还要继续沿接口继承链查找由父接口声明的契约方法。
|
|
||||||
/// 对类或结构体不遍历 <see cref="INamedTypeSymbol.AllInterfaces"/>,避免把仅能通过接口调用的显式实现
|
|
||||||
/// 误判为可由 <c>this.<registry>.<method>(...)</c> 直接访问的方法。
|
|
||||||
/// 这里故意不做去重:同一个语义方法可能同时经由覆盖链、接口继承或显式声明被枚举多次,但当前调用方只使用
|
|
||||||
/// <c>Any</c> 判断“是否存在至少一个可用候选”,因此重复项只会带来额外的符号检查成本,不会改变生成结果或诊断边界。
|
|
||||||
/// </remarks>
|
|
||||||
private static IEnumerable<IMethodSymbol> EnumerateCandidateMethods(
|
|
||||||
INamedTypeSymbol registryType,
|
|
||||||
string registerMethodName)
|
|
||||||
{
|
|
||||||
// Start from the declared registry type so directly declared overloads win the cheap checks
|
|
||||||
// before we expand into inherited declarations.
|
|
||||||
foreach (var method in registryType.GetMembers(registerMethodName).OfType<IMethodSymbol>())
|
|
||||||
yield return method;
|
|
||||||
|
|
||||||
// Concrete registry types can inherit callable implementations from base classes. When the
|
|
||||||
// registry itself is an interface, BaseType is null and this phase intentionally yields nothing.
|
|
||||||
for (var baseType = registryType.BaseType; baseType is not null; baseType = baseType.BaseType)
|
|
||||||
{
|
|
||||||
foreach (var method in baseType.GetMembers(registerMethodName).OfType<IMethodSymbol>())
|
|
||||||
yield return method;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only interface-typed registry members should search interface inheritance. For classes or
|
|
||||||
// structs this avoids accepting explicit interface implementations that generated code cannot
|
|
||||||
// call through `this.<registry>.<method>(...)`. AllInterfaces is already transitive, so the
|
|
||||||
// same semantic contract may appear multiple times; that is safe because the caller only uses Any().
|
|
||||||
if (registryType.TypeKind != TypeKind.Interface)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
foreach (var interfaceType in registryType.AllInterfaces)
|
|
||||||
{
|
|
||||||
foreach (var method in interfaceType.GetMembers(registerMethodName).OfType<IMethodSymbol>())
|
|
||||||
yield return method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetRegistrationAttributeArguments(
|
|
||||||
SourceProductionContext context,
|
|
||||||
ISymbol collectionMember,
|
|
||||||
AttributeData attribute,
|
|
||||||
out string registryMemberName,
|
|
||||||
out string registerMethodName)
|
|
||||||
{
|
|
||||||
registryMemberName = string.Empty;
|
|
||||||
registerMethodName = string.Empty;
|
|
||||||
|
|
||||||
if (attribute.ConstructorArguments.Length != 2 ||
|
|
||||||
attribute.ConstructorArguments[0].Value is not string registryName ||
|
|
||||||
attribute.ConstructorArguments[1].Value is not string methodName)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterExportedCollectionsDiagnostics.InvalidAttributeArguments,
|
|
||||||
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation()
|
|
||||||
?? collectionMember.Locations.FirstOrDefault()
|
|
||||||
?? Location.None,
|
|
||||||
collectionMember.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
registryMemberName = registryName;
|
|
||||||
registerMethodName = methodName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ITypeSymbol? TryGetElementType(ITypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
if (typeSymbol is IArrayTypeSymbol arrayType)
|
|
||||||
return arrayType.ElementType;
|
|
||||||
|
|
||||||
if (typeSymbol is INamedTypeSymbol namedType &&
|
|
||||||
namedType.IsGenericType &&
|
|
||||||
namedType.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
|
|
||||||
{
|
|
||||||
return namedType.TypeArguments[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
var enumerableInterface = typeSymbol.AllInterfaces
|
|
||||||
.FirstOrDefault(interfaceType =>
|
|
||||||
interfaceType.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
|
|
||||||
|
|
||||||
return enumerableInterface?.TypeArguments[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenerateSource(INamedTypeSymbol typeSymbol, IReadOnlyList<RegistrationSpec> registrations)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("// <auto-generated />");
|
|
||||||
builder.AppendLine("#nullable enable");
|
|
||||||
builder.AppendLine();
|
|
||||||
|
|
||||||
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? null
|
|
||||||
: typeSymbol.ContainingNamespace.ToDisplayString();
|
|
||||||
|
|
||||||
if (ns is not null)
|
|
||||||
{
|
|
||||||
builder.AppendLine($"namespace {ns};");
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AppendLine($"{GetTypeDeclarationKeyword(typeSymbol)} {GetTypeDeclarationName(typeSymbol)}");
|
|
||||||
AppendTypeConstraints(builder, typeSymbol);
|
|
||||||
builder.AppendLine("{");
|
|
||||||
builder.AppendLine($" private void {GeneratedMethodName}()");
|
|
||||||
builder.AppendLine(" {");
|
|
||||||
|
|
||||||
foreach (var registration in registrations)
|
|
||||||
{
|
|
||||||
builder.Append(" if (this.");
|
|
||||||
builder.Append(registration.CollectionMemberName);
|
|
||||||
builder.Append(" is not null && this.");
|
|
||||||
builder.Append(registration.RegistryMemberName);
|
|
||||||
builder.AppendLine(" is not null)");
|
|
||||||
builder.AppendLine(" {");
|
|
||||||
builder.Append(" foreach (var __generatedItem in this.");
|
|
||||||
builder.Append(registration.CollectionMemberName);
|
|
||||||
builder.AppendLine(")");
|
|
||||||
builder.AppendLine(" {");
|
|
||||||
builder.Append(" this.");
|
|
||||||
builder.Append(registration.RegistryMemberName);
|
|
||||||
builder.Append('.');
|
|
||||||
builder.Append(registration.RegisterMethodName);
|
|
||||||
builder.AppendLine("(__generatedItem);");
|
|
||||||
builder.AppendLine(" }");
|
|
||||||
builder.AppendLine(" }");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AppendLine(" }");
|
|
||||||
builder.AppendLine("}");
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPartial(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.DeclaringSyntaxReferences
|
|
||||||
.Select(static reference => reference.GetSyntax())
|
|
||||||
.OfType<ClassDeclarationSyntax>()
|
|
||||||
.All(static declaration =>
|
|
||||||
declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetHintName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
var prefix = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? typeSymbol.Name
|
|
||||||
: $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
|
|
||||||
return prefix.Replace('.', '_') + ".AutoRegisterExportedCollections.g.cs";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol switch
|
|
||||||
{
|
|
||||||
{ IsRecord: true, TypeKind: TypeKind.Struct } => "partial record struct",
|
|
||||||
{ IsRecord: true } => "partial record",
|
|
||||||
{ TypeKind: TypeKind.Struct } => "partial struct",
|
|
||||||
{ TypeKind: TypeKind.Class } => "partial class",
|
|
||||||
{ TypeKind: TypeKind.Interface } => "partial interface",
|
|
||||||
_ => throw new NotSupportedException($"Unsupported type: {typeSymbol.TypeKind}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
if (typeSymbol.TypeParameters.Length == 0)
|
|
||||||
return typeSymbol.Name;
|
|
||||||
|
|
||||||
return
|
|
||||||
$"{typeSymbol.Name}<{string.Join(", ", typeSymbol.TypeParameters.Select(static parameter => parameter.Name))}>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendTypeConstraints(StringBuilder builder, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
foreach (var typeParameter in typeSymbol.TypeParameters)
|
|
||||||
{
|
|
||||||
var constraints = new List<string>();
|
|
||||||
|
|
||||||
if (typeParameter.HasReferenceTypeConstraint)
|
|
||||||
{
|
|
||||||
constraints.Add(
|
|
||||||
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
|
|
||||||
? "class?"
|
|
||||||
: "class");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeParameter.HasNotNullConstraint)
|
|
||||||
constraints.Add("notnull");
|
|
||||||
|
|
||||||
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
|
|
||||||
if (typeParameter.HasUnmanagedTypeConstraint)
|
|
||||||
constraints.Add("unmanaged");
|
|
||||||
else if (typeParameter.HasValueTypeConstraint)
|
|
||||||
constraints.Add("struct");
|
|
||||||
|
|
||||||
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>
|
|
||||||
constraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
|
|
||||||
|
|
||||||
if (typeParameter.HasConstructorConstraint)
|
|
||||||
constraints.Add("new()");
|
|
||||||
|
|
||||||
if (constraints.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
builder.Append(" where ");
|
|
||||||
builder.Append(typeParameter.Name);
|
|
||||||
builder.Append(" : ");
|
|
||||||
builder.AppendLine(string.Join(", ", constraints));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TypeCandidate
|
|
||||||
{
|
|
||||||
public TypeCandidate(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
ClassDeclaration = classDeclaration;
|
|
||||||
TypeSymbol = typeSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassDeclarationSyntax ClassDeclaration { get; }
|
|
||||||
|
|
||||||
public INamedTypeSymbol TypeSymbol { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class RegistrationSpec
|
|
||||||
{
|
|
||||||
public RegistrationSpec(string collectionMemberName, string registryMemberName, string registerMethodName)
|
|
||||||
{
|
|
||||||
CollectionMemberName = collectionMemberName;
|
|
||||||
RegistryMemberName = registryMemberName;
|
|
||||||
RegisterMethodName = registerMethodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CollectionMemberName { get; }
|
|
||||||
|
|
||||||
public string RegistryMemberName { get; }
|
|
||||||
|
|
||||||
public string RegisterMethodName { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标记架构模块类型,Source Generator 会根据注册特性生成 <c>Install</c> 方法。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterModuleAttribute : Attribute
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 声明架构模块需要自动注册的模型类型。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterModelAttribute(Type modelType) : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取要注册的模型类型。
|
|
||||||
/// </summary>
|
|
||||||
public Type ModelType { get; } = modelType;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 声明架构模块需要自动注册的系统类型。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterSystemAttribute(Type systemType) : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取要注册的系统类型。
|
|
||||||
/// </summary>
|
|
||||||
public Type SystemType { get; } = systemType;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 声明架构模块需要自动注册的工具类型。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterUtilityAttribute(Type utilityType) : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取要注册的工具类型。
|
|
||||||
/// </summary>
|
|
||||||
public Type UtilityType { get; } = utilityType;
|
|
||||||
}
|
|
||||||
@ -1,373 +0,0 @@
|
|||||||
using GFramework.SourceGenerators.Architectures;
|
|
||||||
using GFramework.SourceGenerators.Tests.Core;
|
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Tests.Architectures;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class AutoRegisterModuleGeneratorTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 验证同一声明上的注册特性会按照源码中的书写顺序生成安装代码。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Module_Install_Method_In_Attribute_Order()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterModelAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterModelAttribute(Type modelType) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterSystemAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterSystemAttribute(Type systemType) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterUtilityAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterUtilityAttribute(Type utilityType) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures
|
|
||||||
{
|
|
||||||
public interface IArchitecture
|
|
||||||
{
|
|
||||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
|
||||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
|
||||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TestApp
|
|
||||||
{
|
|
||||||
using GFramework.Core.Abstractions.Model;
|
|
||||||
using GFramework.Core.Abstractions.Systems;
|
|
||||||
using GFramework.Core.Abstractions.Utility;
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
public sealed class PlayerModel : IModel { }
|
|
||||||
public sealed class CombatSystem : ISystem { }
|
|
||||||
public sealed class AudioUtility : IUtility { }
|
|
||||||
|
|
||||||
[AutoRegisterModule]
|
|
||||||
[RegisterSystem(typeof(CombatSystem))]
|
|
||||||
[RegisterModel(typeof(PlayerModel))]
|
|
||||||
[RegisterUtility(typeof(AudioUtility))]
|
|
||||||
public partial class GameplayModule
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class GameplayModule
|
|
||||||
{
|
|
||||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
|
||||||
{
|
|
||||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
|
||||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
|
||||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterModuleGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 验证 partial 声明分布在多个文件时,生成器仍然会使用稳定的跨文件顺序生成注册代码。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Module_Install_Method_In_Deterministic_Order_Across_Partial_Declarations()
|
|
||||||
{
|
|
||||||
const string commonSource = """
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterModelAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterModelAttribute(Type modelType) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterSystemAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterSystemAttribute(Type systemType) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterUtilityAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterUtilityAttribute(Type utilityType) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures
|
|
||||||
{
|
|
||||||
public interface IArchitecture
|
|
||||||
{
|
|
||||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
|
||||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
|
||||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TestApp
|
|
||||||
{
|
|
||||||
using GFramework.Core.Abstractions.Model;
|
|
||||||
using GFramework.Core.Abstractions.Systems;
|
|
||||||
using GFramework.Core.Abstractions.Utility;
|
|
||||||
|
|
||||||
public sealed class PlayerModel : IModel { }
|
|
||||||
public sealed class CombatSystem : ISystem { }
|
|
||||||
public sealed class AudioUtility : IUtility { }
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string partASource = """
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
// Padding ensures this attribute lives later in the file than the attributes in PartB.
|
|
||||||
// The generator should still place it first because PartA sorts before PartB.
|
|
||||||
// padding 01
|
|
||||||
// padding 02
|
|
||||||
// padding 03
|
|
||||||
// padding 04
|
|
||||||
// padding 05
|
|
||||||
// padding 06
|
|
||||||
// padding 07
|
|
||||||
// padding 08
|
|
||||||
// padding 09
|
|
||||||
// padding 10
|
|
||||||
[AutoRegisterModule]
|
|
||||||
[RegisterUtility(typeof(AudioUtility))]
|
|
||||||
public partial class GameplayModule
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string partBSource = """
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
[RegisterSystem(typeof(CombatSystem))]
|
|
||||||
[RegisterModel(typeof(PlayerModel))]
|
|
||||||
public partial class GameplayModule
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class GameplayModule
|
|
||||||
{
|
|
||||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
|
||||||
{
|
|
||||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
|
||||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
|
||||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterModuleGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources =
|
|
||||||
{
|
|
||||||
("Common.cs", commonSource),
|
|
||||||
("GameplayModule.PartA.cs", partASource),
|
|
||||||
("GameplayModule.PartB.cs", partBSource)
|
|
||||||
},
|
|
||||||
GeneratedSources =
|
|
||||||
{
|
|
||||||
(typeof(AutoRegisterModuleGenerator), "TestApp_GameplayModule.AutoRegisterModule.g.cs", NormalizeLineEndings(expected))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
|
||||||
};
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 验证生成器会保留可空引用、notnull 与 unmanaged 约束。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_Type_Constraints_For_NullableReference_NotNull_And_Unmanaged()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Abstractions.Architectures
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterModelAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterModelAttribute(Type modelType) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterSystemAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterSystemAttribute(Type systemType) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class RegisterUtilityAttribute : Attribute
|
|
||||||
{
|
|
||||||
public RegisterUtilityAttribute(Type utilityType) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures
|
|
||||||
{
|
|
||||||
public interface IArchitecture
|
|
||||||
{
|
|
||||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
|
||||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
|
||||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TestApp
|
|
||||||
{
|
|
||||||
using GFramework.Core.Abstractions.Model;
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
|
|
||||||
public sealed class PlayerModel : IModel { }
|
|
||||||
|
|
||||||
[AutoRegisterModule]
|
|
||||||
[RegisterModel(typeof(PlayerModel))]
|
|
||||||
public partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
|
||||||
where TNullableRef : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class GameplayModule<TNullableRef, TNotNull, TUnmanaged>
|
|
||||||
where TNullableRef : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
|
||||||
{
|
|
||||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<AutoRegisterModuleGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将测试快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">原始快照内容。</param>
|
|
||||||
/// <returns>使用当前平台换行符的快照内容。</returns>
|
|
||||||
private static string NormalizeLineEndings(string content)
|
|
||||||
{
|
|
||||||
return content
|
|
||||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
|
||||||
.Replace("\r", "\n", StringComparison.Ordinal)
|
|
||||||
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -624,7 +624,7 @@ public class ContextGetGeneratorTests
|
|||||||
Sources = { source.Source },
|
Sources = { source.Source },
|
||||||
GeneratedSources =
|
GeneratedSources =
|
||||||
{
|
{
|
||||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected))
|
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
@ -725,7 +725,7 @@ public class ContextGetGeneratorTests
|
|||||||
Sources = { source.Source },
|
Sources = { source.Source },
|
||||||
GeneratedSources =
|
GeneratedSources =
|
||||||
{
|
{
|
||||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected))
|
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
@ -740,14 +740,6 @@ public class ContextGetGeneratorTests
|
|||||||
Assert.Pass();
|
Assert.Pass();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeLineEndings(string content)
|
|
||||||
{
|
|
||||||
return content
|
|
||||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
|
||||||
.Replace("\r", "\n", StringComparison.Ordinal)
|
|
||||||
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
|
public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
### New Rules
|
### New Rules
|
||||||
|
|
||||||
Rule ID | Category | Severity | Notes
|
Rule ID | Category | Severity | Notes
|
||||||
----------------------------|------------------------------------------|----------|--------------------------------
|
----------------------------|------------------------------------|----------|--------------------------------
|
||||||
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
|
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
|
||||||
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
|
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
|
||||||
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
@ -27,11 +27,6 @@
|
|||||||
GF_ConfigSchema_007 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_007 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_008 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_008 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_009 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_009 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_AutoModule_001 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
|
||||||
GF_AutoModule_002 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
|
||||||
GF_AutoModule_003 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
|
||||||
GF_AutoModule_004 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
|
||||||
GF_AutoModule_005 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
|
||||||
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
|
||||||
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
|
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
|
||||||
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
|
||||||
|
|||||||
@ -1,453 +0,0 @@
|
|||||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
using GFramework.SourceGenerators.Diagnostics;
|
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Architectures;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为标记了 <see cref="AutoRegisterModuleAttribute" /> 的模块生成固定顺序的组件注册代码。
|
|
||||||
/// </summary>
|
|
||||||
[Generator]
|
|
||||||
public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
private const string AutoRegisterModuleAttributeMetadataName =
|
|
||||||
$"{PathContests.SourceGeneratorsAbstractionsPath}.Architectures.AutoRegisterModuleAttribute";
|
|
||||||
|
|
||||||
private const string RegisterModelAttributeMetadataName =
|
|
||||||
$"{PathContests.SourceGeneratorsAbstractionsPath}.Architectures.RegisterModelAttribute";
|
|
||||||
|
|
||||||
private const string RegisterSystemAttributeMetadataName =
|
|
||||||
$"{PathContests.SourceGeneratorsAbstractionsPath}.Architectures.RegisterSystemAttribute";
|
|
||||||
|
|
||||||
private const string RegisterUtilityAttributeMetadataName =
|
|
||||||
$"{PathContests.SourceGeneratorsAbstractionsPath}.Architectures.RegisterUtilityAttribute";
|
|
||||||
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
|
|
||||||
static (node, _) => IsCandidate(node),
|
|
||||||
static (syntaxContext, _) => Transform(syntaxContext))
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
return node is ClassDeclarationSyntax classDeclaration &&
|
|
||||||
classDeclaration.AttributeLists
|
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute =>
|
|
||||||
attribute.Name.ToString().Contains("AutoRegisterModule", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TypeCandidate? Transform(GeneratorSyntaxContext context)
|
|
||||||
{
|
|
||||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new TypeCandidate(classDeclaration, typeSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Execute(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
ImmutableArray<TypeCandidate?> candidates)
|
|
||||||
{
|
|
||||||
if (candidates.IsDefaultOrEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var autoRegisterModuleAttribute = compilation.GetTypeByMetadataName(AutoRegisterModuleAttributeMetadataName);
|
|
||||||
var registerModelAttribute = compilation.GetTypeByMetadataName(RegisterModelAttributeMetadataName);
|
|
||||||
var registerSystemAttribute = compilation.GetTypeByMetadataName(RegisterSystemAttributeMetadataName);
|
|
||||||
var registerUtilityAttribute = compilation.GetTypeByMetadataName(RegisterUtilityAttributeMetadataName);
|
|
||||||
var architectureType =
|
|
||||||
compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Architectures.IArchitecture");
|
|
||||||
var modelType = compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Model.IModel");
|
|
||||||
var systemType = compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Systems.ISystem");
|
|
||||||
var utilityType =
|
|
||||||
compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Utility.IUtility");
|
|
||||||
|
|
||||||
if (autoRegisterModuleAttribute is null ||
|
|
||||||
registerModelAttribute is null ||
|
|
||||||
registerSystemAttribute is null ||
|
|
||||||
registerUtilityAttribute is null ||
|
|
||||||
architectureType is null ||
|
|
||||||
modelType is null ||
|
|
||||||
systemType is null ||
|
|
||||||
utilityType is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var candidate in candidates.Where(static candidate => candidate is not null)
|
|
||||||
.Select(static candidate => candidate!))
|
|
||||||
{
|
|
||||||
if (!candidate.TypeSymbol.GetAttributes().Any(attribute =>
|
|
||||||
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, autoRegisterModuleAttribute)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CanGenerateForType(context, candidate))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (HasInstallConflict(context, candidate, architectureType))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var registrations = CollectRegistrations(
|
|
||||||
context,
|
|
||||||
candidate.TypeSymbol,
|
|
||||||
registerModelAttribute,
|
|
||||||
registerSystemAttribute,
|
|
||||||
registerUtilityAttribute,
|
|
||||||
modelType,
|
|
||||||
systemType,
|
|
||||||
utilityType);
|
|
||||||
|
|
||||||
if (registrations.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, registrations));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanGenerateForType(SourceProductionContext context, TypeCandidate candidate)
|
|
||||||
{
|
|
||||||
if (candidate.TypeSymbol.ContainingType is not null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterModuleDiagnostics.NestedClassNotSupported,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsPartial(candidate.TypeSymbol))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
CommonDiagnostics.ClassMustBePartial,
|
|
||||||
candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasInstallConflict(
|
|
||||||
SourceProductionContext context,
|
|
||||||
TypeCandidate candidate,
|
|
||||||
INamedTypeSymbol architectureType)
|
|
||||||
{
|
|
||||||
var installMethod = candidate.TypeSymbol.GetMembers("Install")
|
|
||||||
.OfType<IMethodSymbol>()
|
|
||||||
.FirstOrDefault(method =>
|
|
||||||
!method.IsImplicitlyDeclared &&
|
|
||||||
method.Parameters.Length == 1 &&
|
|
||||||
method.TypeParameters.Length == 0 &&
|
|
||||||
method.ReturnsVoid &&
|
|
||||||
method.Parameters[0].Type is ITypeSymbol parameterType &&
|
|
||||||
SymbolEqualityComparer.Default.Equals(parameterType, architectureType));
|
|
||||||
|
|
||||||
if (installMethod is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterModuleDiagnostics.InstallMethodConflict,
|
|
||||||
installMethod.Locations.FirstOrDefault() ?? candidate.ClassDeclaration.Identifier.GetLocation(),
|
|
||||||
candidate.TypeSymbol.Name));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<RegistrationSpec> CollectRegistrations(
|
|
||||||
SourceProductionContext context,
|
|
||||||
INamedTypeSymbol typeSymbol,
|
|
||||||
INamedTypeSymbol registerModelAttribute,
|
|
||||||
INamedTypeSymbol registerSystemAttribute,
|
|
||||||
INamedTypeSymbol registerUtilityAttribute,
|
|
||||||
INamedTypeSymbol modelType,
|
|
||||||
INamedTypeSymbol systemType,
|
|
||||||
INamedTypeSymbol utilityType)
|
|
||||||
{
|
|
||||||
var registrations = new List<RegistrationSpec>();
|
|
||||||
|
|
||||||
foreach (var attribute in typeSymbol.GetAttributes()
|
|
||||||
// Roslyn 会把 partial 类型上的属性合并到同一个集合中。
|
|
||||||
// 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。
|
|
||||||
.OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal)
|
|
||||||
.ThenBy(GetAttributeOrder)
|
|
||||||
.ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute))
|
|
||||||
{
|
|
||||||
if (TryCreateRegistration(
|
|
||||||
context,
|
|
||||||
typeSymbol,
|
|
||||||
attribute,
|
|
||||||
"RegisterModelAttribute",
|
|
||||||
modelType,
|
|
||||||
RegistrationKind.Model,
|
|
||||||
out var registration))
|
|
||||||
{
|
|
||||||
registrations.Add(registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerSystemAttribute))
|
|
||||||
{
|
|
||||||
if (TryCreateRegistration(
|
|
||||||
context,
|
|
||||||
typeSymbol,
|
|
||||||
attribute,
|
|
||||||
"RegisterSystemAttribute",
|
|
||||||
systemType,
|
|
||||||
RegistrationKind.System,
|
|
||||||
out var registration))
|
|
||||||
{
|
|
||||||
registrations.Add(registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerUtilityAttribute))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (TryCreateRegistration(
|
|
||||||
context,
|
|
||||||
typeSymbol,
|
|
||||||
attribute,
|
|
||||||
"RegisterUtilityAttribute",
|
|
||||||
utilityType,
|
|
||||||
RegistrationKind.Utility,
|
|
||||||
out var utilityRegistration))
|
|
||||||
{
|
|
||||||
registrations.Add(utilityRegistration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return registrations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryCreateRegistration(
|
|
||||||
SourceProductionContext context,
|
|
||||||
INamedTypeSymbol ownerType,
|
|
||||||
AttributeData attribute,
|
|
||||||
string attributeDisplayName,
|
|
||||||
INamedTypeSymbol expectedInterface,
|
|
||||||
RegistrationKind registrationKind,
|
|
||||||
out RegistrationSpec registration)
|
|
||||||
{
|
|
||||||
registration = default;
|
|
||||||
|
|
||||||
if (attribute.ConstructorArguments.Length != 1 ||
|
|
||||||
attribute.ConstructorArguments[0].Value is not INamedTypeSymbol componentType)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterModuleDiagnostics.RegistrationTypeRequired,
|
|
||||||
GetAttributeLocation(attribute),
|
|
||||||
attributeDisplayName,
|
|
||||||
ownerType.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!componentType.IsAssignableTo(expectedInterface))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterModuleDiagnostics.RegistrationTypeMustImplementExpectedInterface,
|
|
||||||
GetAttributeLocation(attribute),
|
|
||||||
componentType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
|
|
||||||
ownerType.Name,
|
|
||||||
expectedInterface.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentType.IsAbstract ||
|
|
||||||
!componentType.InstanceConstructors.Any(ctor =>
|
|
||||||
ctor.Parameters.Length == 0 &&
|
|
||||||
ctor.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal))
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
AutoRegisterModuleDiagnostics.RegistrationTypeMustHaveParameterlessConstructor,
|
|
||||||
GetAttributeLocation(attribute),
|
|
||||||
componentType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
|
|
||||||
ownerType.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
registration = new RegistrationSpec(
|
|
||||||
registrationKind,
|
|
||||||
componentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenerateSource(INamedTypeSymbol typeSymbol, IReadOnlyList<RegistrationSpec> registrations)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("// <auto-generated />");
|
|
||||||
builder.AppendLine("#nullable enable");
|
|
||||||
builder.AppendLine();
|
|
||||||
|
|
||||||
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? null
|
|
||||||
: typeSymbol.ContainingNamespace.ToDisplayString();
|
|
||||||
|
|
||||||
if (ns is not null)
|
|
||||||
{
|
|
||||||
builder.AppendLine($"namespace {ns};");
|
|
||||||
builder.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AppendLine($"{GetTypeDeclarationKeyword(typeSymbol)} {GetTypeDeclarationName(typeSymbol)}");
|
|
||||||
AppendTypeConstraints(builder, typeSymbol);
|
|
||||||
builder.AppendLine("{");
|
|
||||||
builder.AppendLine(
|
|
||||||
$" public void Install(global::{PathContests.CoreAbstractionsNamespace}.Architectures.IArchitecture architecture)");
|
|
||||||
builder.AppendLine(" {");
|
|
||||||
|
|
||||||
foreach (var registration in registrations)
|
|
||||||
{
|
|
||||||
builder.Append(" architecture.");
|
|
||||||
builder.Append(registration.Kind switch
|
|
||||||
{
|
|
||||||
RegistrationKind.Model => "RegisterModel",
|
|
||||||
RegistrationKind.System => "RegisterSystem",
|
|
||||||
RegistrationKind.Utility => "RegisterUtility",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(registration.Kind))
|
|
||||||
});
|
|
||||||
builder.Append("(new ");
|
|
||||||
builder.Append(registration.ComponentTypeDisplayName);
|
|
||||||
builder.AppendLine("());");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.AppendLine(" }");
|
|
||||||
builder.AppendLine("}");
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetHintName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
var prefix = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
||||||
? typeSymbol.Name
|
|
||||||
: $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
|
|
||||||
return prefix.Replace('.', '_') + ".AutoRegisterModule.g.cs";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPartial(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.DeclaringSyntaxReferences
|
|
||||||
.Select(static reference => reference.GetSyntax())
|
|
||||||
.OfType<ClassDeclarationSyntax>()
|
|
||||||
.All(static declaration =>
|
|
||||||
declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetAttributeOrder(AttributeData attribute)
|
|
||||||
{
|
|
||||||
return attribute.ApplicationSyntaxReference?.Span.Start ?? int.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAttributeSyntaxTreeOrderKey(AttributeData attribute)
|
|
||||||
{
|
|
||||||
var syntaxTree = attribute.ApplicationSyntaxReference?.SyntaxTree;
|
|
||||||
if (syntaxTree is null)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(syntaxTree.FilePath))
|
|
||||||
return syntaxTree.FilePath;
|
|
||||||
|
|
||||||
// In-memory compilations may not assign file paths. Fall back to the syntax tree text so
|
|
||||||
// attributes from different partial declarations still get a deterministic cross-file order.
|
|
||||||
return syntaxTree.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAttributeTypeOrderKey(AttributeData attribute)
|
|
||||||
{
|
|
||||||
return attribute.ConstructorArguments.FirstOrDefault().Value is INamedTypeSymbol componentType
|
|
||||||
? componentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
|
||||||
: string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Location GetAttributeLocation(AttributeData attribute)
|
|
||||||
{
|
|
||||||
return attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? Location.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.IsRecord
|
|
||||||
? typeSymbol.TypeKind == TypeKind.Struct ? "partial record struct" : "partial record"
|
|
||||||
: typeSymbol.TypeKind == TypeKind.Struct
|
|
||||||
? "partial struct"
|
|
||||||
: "partial class";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
if (typeSymbol.TypeParameters.Length == 0)
|
|
||||||
return typeSymbol.Name;
|
|
||||||
|
|
||||||
return
|
|
||||||
$"{typeSymbol.Name}<{string.Join(", ", typeSymbol.TypeParameters.Select(static parameter => parameter.Name))}>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendTypeConstraints(StringBuilder builder, INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
foreach (var typeParameter in typeSymbol.TypeParameters)
|
|
||||||
{
|
|
||||||
var constraints = new List<string>();
|
|
||||||
|
|
||||||
if (typeParameter.HasReferenceTypeConstraint)
|
|
||||||
{
|
|
||||||
constraints.Add(
|
|
||||||
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
|
|
||||||
? "class?"
|
|
||||||
: "class");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeParameter.HasNotNullConstraint)
|
|
||||||
constraints.Add("notnull");
|
|
||||||
|
|
||||||
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
|
|
||||||
if (typeParameter.HasUnmanagedTypeConstraint)
|
|
||||||
constraints.Add("unmanaged");
|
|
||||||
else if (typeParameter.HasValueTypeConstraint)
|
|
||||||
constraints.Add("struct");
|
|
||||||
|
|
||||||
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>
|
|
||||||
constraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
|
|
||||||
|
|
||||||
if (typeParameter.HasConstructorConstraint)
|
|
||||||
constraints.Add("new()");
|
|
||||||
|
|
||||||
if (constraints.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
builder.Append(" where ");
|
|
||||||
builder.Append(typeParameter.Name);
|
|
||||||
builder.Append(" : ");
|
|
||||||
builder.AppendLine(string.Join(", ", constraints));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record TypeCandidate(ClassDeclarationSyntax ClassDeclaration, INamedTypeSymbol TypeSymbol);
|
|
||||||
|
|
||||||
private readonly record struct RegistrationSpec(RegistrationKind Kind, string ComponentTypeDisplayName);
|
|
||||||
|
|
||||||
private enum RegistrationKind
|
|
||||||
{
|
|
||||||
Model,
|
|
||||||
System,
|
|
||||||
Utility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
|
|
||||||
namespace GFramework.SourceGenerators.Diagnostics;
|
|
||||||
|
|
||||||
internal static class AutoRegisterModuleDiagnostics
|
|
||||||
{
|
|
||||||
private const string Category = $"{PathContests.SourceGeneratorsPath}.Architecture";
|
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
|
||||||
"GF_AutoModule_001",
|
|
||||||
"AutoRegisterModule does not support nested classes",
|
|
||||||
"AutoRegisterModule does not support nested class '{0}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor RegistrationTypeRequired = new(
|
|
||||||
"GF_AutoModule_002",
|
|
||||||
"Registration attribute requires a concrete type",
|
|
||||||
"Attribute '{0}' on '{1}' requires a concrete type argument",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor RegistrationTypeMustImplementExpectedInterface = new(
|
|
||||||
"GF_AutoModule_003",
|
|
||||||
"Registration type does not implement the expected interface",
|
|
||||||
"Type '{0}' used by '{1}' must implement '{2}'",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor RegistrationTypeMustHaveParameterlessConstructor = new(
|
|
||||||
"GF_AutoModule_004",
|
|
||||||
"Registration type must have an accessible parameterless constructor",
|
|
||||||
"Type '{0}' used by '{1}' must have an accessible parameterless constructor",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor InstallMethodConflict = new(
|
|
||||||
"GF_AutoModule_005",
|
|
||||||
"Install method conflicts with generated code",
|
|
||||||
"Class '{0}' already defines 'Install(IArchitecture)', which conflicts with AutoRegisterModule generated code",
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user