diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs
new file mode 100644
index 00000000..1c2699ae
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoRegisterExportedCollectionsAttribute.cs
@@ -0,0 +1,9 @@
+namespace GFramework.Godot.SourceGenerators.Abstractions;
+
+///
+/// 标记类型允许为带映射特性的导出集合生成批量注册代码。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class AutoRegisterExportedCollectionsAttribute : Attribute
+{
+}
diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs
new file mode 100644
index 00000000..3b73af7b
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoSceneAttribute.cs
@@ -0,0 +1,13 @@
+namespace GFramework.Godot.SourceGenerators.Abstractions;
+
+///
+/// 标记场景根节点类型,Source Generator 会生成场景行为样板代码。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class AutoSceneAttribute(string key) : Attribute
+{
+ ///
+ /// 获取场景键。
+ ///
+ public string Key { get; } = key;
+}
diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs
new file mode 100644
index 00000000..56957fb8
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/AutoUiPageAttribute.cs
@@ -0,0 +1,18 @@
+namespace GFramework.Godot.SourceGenerators.Abstractions;
+
+///
+/// 标记 UI 页面类型,Source Generator 会生成页面行为样板代码。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class AutoUiPageAttribute(string key, string layerName) : Attribute
+{
+ ///
+ /// 获取 UI 键。
+ ///
+ public string Key { get; } = key;
+
+ ///
+ /// 获取 UiLayer 枚举成员名称。
+ ///
+ public string LayerName { get; } = layerName;
+}
diff --git a/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs
new file mode 100644
index 00000000..dd809fc4
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Abstractions/UI/RegisterExportedCollectionAttribute.cs
@@ -0,0 +1,19 @@
+namespace GFramework.Godot.SourceGenerators.Abstractions;
+
+///
+/// 声明导出集合应当转发到哪个注册器成员及其方法。
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
+public sealed class RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName)
+ : Attribute
+{
+ ///
+ /// 获取注册器字段或属性名称。
+ ///
+ public string RegistryMemberName { get; } = registryMemberName;
+
+ ///
+ /// 获取注册方法名称。
+ ///
+ public string RegisterMethodName { get; } = registerMethodName;
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs
new file mode 100644
index 00000000..c60d1f18
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs
@@ -0,0 +1,85 @@
+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 owner, string key)
+ where T : Node
+ {
+ return null!;
+ }
+ }
+ }
+
+ namespace TestApp
+ {
+ [AutoScene("Gameplay")]
+ public partial class GameplayRoot : Node2D
+ {
+ }
+ }
+ """;
+
+ const string expected = """
+ //
+ #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.RunAsync(
+ source,
+ ("TestApp_GameplayRoot.AutoScene.g.cs", expected));
+ }
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs
new file mode 100644
index 00000000..4b9f41c1
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs
@@ -0,0 +1,97 @@
+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 owner, string key, UiLayer layer)
+ where T : CanvasItem
+ {
+ return null!;
+ }
+ }
+ }
+
+ namespace TestApp
+ {
+ [AutoUiPage("MainMenu", "Page")]
+ public partial class MainMenu : Control
+ {
+ }
+ }
+ """;
+
+ const string expected = """
+ //
+ #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.RunAsync(
+ source,
+ ("TestApp_MainMenu.AutoUiPage.g.cs", expected));
+ }
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs
new file mode 100644
index 00000000..0202b824
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs
@@ -0,0 +1,70 @@
+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 = """
+ 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 List Values { get; } = new();
+ }
+ }
+ """;
+
+ const string expected = """
+ //
+ #nullable enable
+
+ namespace TestApp;
+
+ partial class Bootstrapper
+ {
+ private void __RegisterExportedCollections_Generated()
+ {
+ foreach (var item in Values)
+ {
+ _registry.Register(item);
+ }
+ }
+ }
+
+ """;
+
+ await GeneratorTest.RunAsync(
+ source,
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ }
+}
diff --git a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md
index c4a29301..42e69dc7 100644
--- a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -3,21 +3,28 @@
### New Rules
- Rule ID | Category | Severity | Notes
------------------------------|------------------|----------|---------------------------
- GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics
- GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics
- GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics
- GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
- GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
- GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics
- GF_Godot_BindNodeSignal_001 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_002 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_003 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_004 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_005 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_006 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
- GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ Rule ID | Category | Severity | Notes
+-----------------------------|------------------------------------------------|----------|--------------------------------------------
+ GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics
+ GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics
+ GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics
+ GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
+ GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
+ GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics
+ GF_Godot_BindNodeSignal_001 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_002 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_003 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_004 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_005 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_006 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
+ GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics
+ 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_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
diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs
new file mode 100644
index 00000000..4fc9f874
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs
@@ -0,0 +1,236 @@
+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;
+
+///
+/// 为标记了 [AutoScene] 的 Godot 节点生成场景行为样板。
+///
+[Generator]
+public sealed class AutoSceneGenerator : IIncrementalGenerator
+{
+ private const string AutoSceneAttributeMetadataName =
+ $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute";
+
+ 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 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 (attribute.ConstructorArguments.Length != 1 || attribute.ConstructorArguments[0].Value is not string 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 string GenerateSource(INamedTypeSymbol typeSymbol, string key)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine("// ");
+ 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()
+ .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();
+
+ if (typeParameter.HasReferenceTypeConstraint)
+ constraints.Add("class");
+
+ 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; }
+ }
+}
diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs
new file mode 100644
index 00000000..18f82597
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators/Behavior/AutoUiPageGenerator.cs
@@ -0,0 +1,282 @@
+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;
+
+///
+/// 为标记了 [AutoUiPage] 的 Godot CanvasItem 生成页面行为样板。
+///
+[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 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)
+ {
+ 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("// ");
+ 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()
+ .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();
+
+ if (typeParameter.HasReferenceTypeConstraint)
+ constraints.Add("class");
+
+ 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; }
+ }
+}
diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs
new file mode 100644
index 00000000..fd78d27a
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs
@@ -0,0 +1,32 @@
+using GFramework.SourceGenerators.Common.Constants;
+
+namespace GFramework.Godot.SourceGenerators.Diagnostics;
+
+internal static class AutoBehaviorDiagnostics
+{
+ private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Behavior";
+
+ 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);
+
+ 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);
+
+ 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);
+}
diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs
new file mode 100644
index 00000000..bb449e0f
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs
@@ -0,0 +1,40 @@
+using GFramework.SourceGenerators.Common.Constants;
+
+namespace GFramework.Godot.SourceGenerators.Diagnostics;
+
+internal static class AutoRegisterExportedCollectionsDiagnostics
+{
+ private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Registration";
+
+ 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);
+
+ 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);
+
+ 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);
+
+ 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);
+}
diff --git a/GFramework.Godot.SourceGenerators/GlobalUsings.cs b/GFramework.Godot.SourceGenerators/GlobalUsings.cs
index 0413980e..9efb3c18 100644
--- a/GFramework.Godot.SourceGenerators/GlobalUsings.cs
+++ b/GFramework.Godot.SourceGenerators/GlobalUsings.cs
@@ -12,10 +12,12 @@
// limitations under the License.
global using System;
+global using System.Collections.Immutable;
global using System.Collections.Generic;
global using System.Linq;
+global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
-global using Microsoft.CodeAnalysis.CSharp.Syntax;
\ No newline at end of file
+global using Microsoft.CodeAnalysis.CSharp.Syntax;
diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs
new file mode 100644
index 00000000..a051ff2d
--- /dev/null
+++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs
@@ -0,0 +1,405 @@
+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;
+
+///
+/// 为导出集合生成批量注册样板方法。
+///
+[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";
+
+ 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 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!))
+ {
+ 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,
+ 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 CollectRegistrations(
+ SourceProductionContext context,
+ INamedTypeSymbol typeSymbol,
+ INamedTypeSymbol registerCollectionAttribute,
+ INamedTypeSymbol enumerableType)
+ {
+ var registrations = new List();
+
+ 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, typeSymbol, member, attribute, enumerableType, out var registration))
+ continue;
+
+ registrations.Add(registration);
+ }
+
+ return registrations;
+ }
+
+ private static bool TryCreateRegistration(
+ SourceProductionContext context,
+ INamedTypeSymbol ownerType,
+ ISymbol collectionMember,
+ AttributeData attribute,
+ INamedTypeSymbol enumerableType,
+ out RegistrationSpec registration)
+ {
+ registration = null!;
+
+ 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(attribute, out var registryMemberName, out var registerMethodName))
+ return false;
+
+ var registryMember = ownerType.GetMembers(registryMemberName)
+ .FirstOrDefault(member => member is IFieldSymbol or IPropertySymbol);
+
+ if (registryMember is null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AutoRegisterExportedCollectionsDiagnostics.RegistryMemberNotFound,
+ collectionMember.Locations.FirstOrDefault() ?? Location.None,
+ registryMemberName,
+ collectionMember.Name,
+ ownerType.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);
+ var hasCompatibleMethod = registryType.GetMembers(registerMethodName)
+ .OfType()
+ .Any(method =>
+ !method.IsStatic &&
+ method.Parameters.Length == 1 &&
+ (elementType is null || elementType.IsAssignableTo(method.Parameters[0].Type as INamedTypeSymbol)));
+
+ 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 TryGetRegistrationAttributeArguments(
+ 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)
+ {
+ 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 registrations)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine("// ");
+ 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(" foreach (var item in ");
+ builder.Append(registration.CollectionMemberName);
+ builder.AppendLine(")");
+ builder.AppendLine(" {");
+ builder.Append(" ");
+ builder.Append(registration.RegistryMemberName);
+ builder.Append('.');
+ builder.Append(registration.RegisterMethodName);
+ builder.AppendLine("(item);");
+ builder.AppendLine(" }");
+ }
+
+ builder.AppendLine(" }");
+ builder.AppendLine("}");
+ return builder.ToString();
+ }
+
+ private static bool IsPartial(INamedTypeSymbol typeSymbol)
+ {
+ return typeSymbol.DeclaringSyntaxReferences
+ .Select(static reference => reference.GetSyntax())
+ .OfType()
+ .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.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();
+
+ if (typeParameter.HasReferenceTypeConstraint)
+ constraints.Add("class");
+
+ 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; }
+ }
+}
diff --git a/GFramework.SourceGenerators.Abstractions/Architectures/AutoRegisterModuleAttribute.cs b/GFramework.SourceGenerators.Abstractions/Architectures/AutoRegisterModuleAttribute.cs
new file mode 100644
index 00000000..04daaa46
--- /dev/null
+++ b/GFramework.SourceGenerators.Abstractions/Architectures/AutoRegisterModuleAttribute.cs
@@ -0,0 +1,9 @@
+namespace GFramework.SourceGenerators.Abstractions.Architectures;
+
+///
+/// 标记架构模块类型,Source Generator 会根据注册特性生成 Install 方法。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class AutoRegisterModuleAttribute : Attribute
+{
+}
diff --git a/GFramework.SourceGenerators.Abstractions/Architectures/RegisterModelAttribute.cs b/GFramework.SourceGenerators.Abstractions/Architectures/RegisterModelAttribute.cs
new file mode 100644
index 00000000..6c47d8b1
--- /dev/null
+++ b/GFramework.SourceGenerators.Abstractions/Architectures/RegisterModelAttribute.cs
@@ -0,0 +1,13 @@
+namespace GFramework.SourceGenerators.Abstractions.Architectures;
+
+///
+/// 声明架构模块需要自动注册的模型类型。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
+public sealed class RegisterModelAttribute(Type modelType) : Attribute
+{
+ ///
+ /// 获取要注册的模型类型。
+ ///
+ public Type ModelType { get; } = modelType;
+}
diff --git a/GFramework.SourceGenerators.Abstractions/Architectures/RegisterSystemAttribute.cs b/GFramework.SourceGenerators.Abstractions/Architectures/RegisterSystemAttribute.cs
new file mode 100644
index 00000000..7119db2f
--- /dev/null
+++ b/GFramework.SourceGenerators.Abstractions/Architectures/RegisterSystemAttribute.cs
@@ -0,0 +1,13 @@
+namespace GFramework.SourceGenerators.Abstractions.Architectures;
+
+///
+/// 声明架构模块需要自动注册的系统类型。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
+public sealed class RegisterSystemAttribute(Type systemType) : Attribute
+{
+ ///
+ /// 获取要注册的系统类型。
+ ///
+ public Type SystemType { get; } = systemType;
+}
diff --git a/GFramework.SourceGenerators.Abstractions/Architectures/RegisterUtilityAttribute.cs b/GFramework.SourceGenerators.Abstractions/Architectures/RegisterUtilityAttribute.cs
new file mode 100644
index 00000000..88e5d93c
--- /dev/null
+++ b/GFramework.SourceGenerators.Abstractions/Architectures/RegisterUtilityAttribute.cs
@@ -0,0 +1,13 @@
+namespace GFramework.SourceGenerators.Abstractions.Architectures;
+
+///
+/// 声明架构模块需要自动注册的工具类型。
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
+public sealed class RegisterUtilityAttribute(Type utilityType) : Attribute
+{
+ ///
+ /// 获取要注册的工具类型。
+ ///
+ public Type UtilityType { get; } = utilityType;
+}
diff --git a/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs
new file mode 100644
index 00000000..2ced1eed
--- /dev/null
+++ b/GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs
@@ -0,0 +1,108 @@
+using GFramework.SourceGenerators.Architectures;
+using GFramework.SourceGenerators.Tests.Core;
+
+namespace GFramework.SourceGenerators.Tests.Architectures;
+
+[TestFixture]
+public class AutoRegisterModuleGeneratorTests
+{
+ [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 model) where T : GFramework.Core.Abstractions.Model.IModel;
+ T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
+ T RegisterUtility(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 = """
+ //
+ #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.RunAsync(
+ source,
+ ("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
+ }
+}
diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
index c8fec0f1..c8804f58 100644
--- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
@@ -624,7 +624,7 @@ public class ContextGetGeneratorTests
Sources = { source.Source },
GeneratedSources =
{
- (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
+ (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected))
}
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
@@ -725,7 +725,7 @@ public class ContextGetGeneratorTests
Sources = { source.Source },
GeneratedSources =
{
- (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
+ (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected))
}
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
@@ -740,6 +740,14 @@ public class ContextGetGeneratorTests
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]
public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
{
@@ -1266,4 +1274,4 @@ public class ContextGetGeneratorTests
("TestApp_InventoryPanel.ContextGet.g.cs", expected));
Assert.Pass();
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
index df517406..d55048c9 100644
--- a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -3,33 +3,38 @@
### New Rules
- Rule ID | Category | Severity | Notes
-----------------------------|------------------------------------|----------|--------------------------------
- GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
- GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
- GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
- GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
- GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
- GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
- GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
- GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
- GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
- GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
- GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
- GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
- GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
- GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_007 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_008 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_ConfigSchema_009 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
- GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
- GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
- GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
- GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
- GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
- GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer
+ Rule ID | Category | Severity | Notes
+----------------------------|------------------------------------------|----------|--------------------------------
+ GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
+ GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
+ GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
+ GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
+ GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
+ GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
+ GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
+ GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
+ GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
+ GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
+ GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
+ GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
+ GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
+ GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_007 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
+ GF_ConfigSchema_008 | 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_002 | GFramework.Priority | Warning | PriorityDiagnostic
+ GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
+ GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
+ GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
+ GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer
diff --git a/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs b/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs
new file mode 100644
index 00000000..7d5832a1
--- /dev/null
+++ b/GFramework.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs
@@ -0,0 +1,416 @@
+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;
+
+///
+/// 为标记了 的模块生成固定顺序的组件注册代码。
+///
+[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 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()
+ .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 CollectRegistrations(
+ SourceProductionContext context,
+ INamedTypeSymbol typeSymbol,
+ INamedTypeSymbol registerModelAttribute,
+ INamedTypeSymbol registerSystemAttribute,
+ INamedTypeSymbol registerUtilityAttribute,
+ INamedTypeSymbol modelType,
+ INamedTypeSymbol systemType,
+ INamedTypeSymbol utilityType)
+ {
+ var registrations = new List();
+
+ foreach (var attribute in typeSymbol.GetAttributes().OrderBy(GetAttributeOrder))
+ {
+ 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 registrations)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine("// ");
+ 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()
+ .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 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();
+
+ if (typeParameter.HasReferenceTypeConstraint)
+ constraints.Add("class");
+
+ 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
+ }
+}
diff --git a/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs b/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs
new file mode 100644
index 00000000..b2a58ce3
--- /dev/null
+++ b/GFramework.SourceGenerators/Diagnostics/AutoRegisterModuleDiagnostics.cs
@@ -0,0 +1,48 @@
+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 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);
+
+ 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);
+}