mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(generator): 添加代码生成器诊断规则和测试用例
- 定义了 Godot 源代码生成器的诊断规则表格 - 添加了上下文获取生成器的全面单元测试 - 实现了自动生成行为和注册导出集合的诊断功能 - 配置了全局 using 语句简化代码生成器实现 - 添加了完整的分析器发布跟踪文档记录新规则
This commit is contained in:
parent
83cceed57b
commit
eb307bf188
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记类型允许为带映射特性的导出集合生成批量注册代码。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -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>(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<int> Values { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string expected = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace TestApp;
|
||||||
|
|
||||||
|
partial class Bootstrapper
|
||||||
|
{
|
||||||
|
private void __RegisterExportedCollections_Generated()
|
||||||
|
{
|
||||||
|
foreach (var item in Values)
|
||||||
|
{
|
||||||
|
_registry.Register(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
|
source,
|
||||||
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,21 +3,28 @@
|
|||||||
|
|
||||||
### 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
|
||||||
GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
|
GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
|
||||||
GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics
|
GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics
|
||||||
GF_Godot_BindNodeSignal_001 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_001 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_002 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_002 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_003 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_003 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_004 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_004 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_005 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_005 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_006 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_006 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||||
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_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
|
||||||
|
|||||||
236
GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs
Normal file
236
GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为标记了 <c>[AutoScene]</c> 的 Godot 节点生成场景行为样板。
|
||||||
|
/// </summary>
|
||||||
|
[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<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 (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("// <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("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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
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("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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -12,10 +12,12 @@
|
|||||||
// 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;
|
||||||
global using Microsoft.CodeAnalysis.CSharp;
|
global using Microsoft.CodeAnalysis.CSharp;
|
||||||
global using Microsoft.CodeAnalysis.CSharp.Syntax;
|
global using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为导出集合生成批量注册样板方法。
|
||||||
|
/// </summary>
|
||||||
|
[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<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!))
|
||||||
|
{
|
||||||
|
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<RegistrationSpec> CollectRegistrations(
|
||||||
|
SourceProductionContext context,
|
||||||
|
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, 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<IMethodSymbol>()
|
||||||
|
.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<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(" 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<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.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("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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Abstractions.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记架构模块类型,Source Generator 会根据注册特性生成 <c>Install</c> 方法。
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoRegisterModuleAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -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>(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -624,7 +624,7 @@ public class ContextGetGeneratorTests
|
|||||||
Sources = { source.Source },
|
Sources = { source.Source },
|
||||||
GeneratedSources =
|
GeneratedSources =
|
||||||
{
|
{
|
||||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(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", expected)
|
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", NormalizeLineEndings(expected))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
@ -740,6 +740,14 @@ 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()
|
||||||
{
|
{
|
||||||
@ -1266,4 +1274,4 @@ public class ContextGetGeneratorTests
|
|||||||
("TestApp_InventoryPanel.ContextGet.g.cs", expected));
|
("TestApp_InventoryPanel.ContextGet.g.cs", expected));
|
||||||
Assert.Pass();
|
Assert.Pass();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,33 +3,38 @@
|
|||||||
|
|
||||||
### 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
|
||||||
GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
||||||
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
||||||
GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
||||||
GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
||||||
GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
||||||
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
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_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_AutoModule_001 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
|
GF_AutoModule_002 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_AutoModule_003 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_AutoModule_004 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_AutoModule_005 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer
|
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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
/// <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().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<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 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("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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user