mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(godot): 添加AutoScene和AutoRegisterExportedCollections源代码生成器
- 实现AutoSceneGenerator为标记了[AutoScene]特性的Godot节点生成场景行为样板代码 - 实现AutoRegisterExportedCollectionsGenerator为导出集合生成批量注册样板方法 - 添加AutoBehaviorDiagnostics和AutoRegisterExportedCollectionsDiagnostics诊断描述符 - 创建AnalyzerReleases.Unshipped.md文件跟踪新的分析器规则 - 添加完整的单元测试覆盖两个生成器的功能和错误情况 - 更新.gitignore文件排除dotnet-home和脚本缓存目录
This commit is contained in:
parent
eb307bf188
commit
80acf84e95
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,8 @@ riderModule.iml
|
|||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
GFramework.sln.DotSettings.user
|
GFramework.sln.DotSettings.user
|
||||||
.idea/
|
.idea/
|
||||||
|
dotnet-home/
|
||||||
|
scripts/__pycache__/
|
||||||
# ai
|
# ai
|
||||||
opencode.json
|
opencode.json
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|||||||
@ -82,4 +82,52 @@ public class AutoSceneGeneratorTests
|
|||||||
source,
|
source,
|
||||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
|
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using System;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoSceneAttribute : Attribute
|
||||||
|
{
|
||||||
|
public AutoSceneAttribute() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Godot
|
||||||
|
{
|
||||||
|
public class Node { }
|
||||||
|
public class Node2D : Node { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
[{|#0:AutoScene|}]
|
||||||
|
public partial class GameplayRoot : Node2D
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
TestState =
|
||||||
|
{
|
||||||
|
Sources = { source }
|
||||||
|
},
|
||||||
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
|
};
|
||||||
|
|
||||||
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoBehavior_004", DiagnosticSeverity.Error)
|
||||||
|
.WithLocation(0)
|
||||||
|
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
|
||||||
|
|
||||||
|
await test.RunAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,4 +67,58 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reports_Diagnostic_When_Collection_Element_Type_Cannot_Be_Inferred()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public sealed class IntRegistry
|
||||||
|
{
|
||||||
|
public void Register(int value) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[AutoRegisterExportedCollections]
|
||||||
|
public partial class Bootstrapper
|
||||||
|
{
|
||||||
|
private readonly IntRegistry _registry = new();
|
||||||
|
|
||||||
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
|
public IEnumerable {|#0:Values|} { get; } = new ArrayList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
TestState =
|
||||||
|
{
|
||||||
|
Sources = { source }
|
||||||
|
},
|
||||||
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
|
};
|
||||||
|
|
||||||
|
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_005", DiagnosticSeverity.Error)
|
||||||
|
.WithLocation(0)
|
||||||
|
.WithArguments("Values"));
|
||||||
|
|
||||||
|
await test.RunAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,9 @@
|
|||||||
GF_AutoBehavior_001 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
GF_AutoBehavior_001 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
||||||
GF_AutoBehavior_002 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
GF_AutoBehavior_002 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
||||||
GF_AutoBehavior_003 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
GF_AutoBehavior_003 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
||||||
|
GF_AutoBehavior_004 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics
|
||||||
GF_AutoExport_001 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
GF_AutoExport_001 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
||||||
GF_AutoExport_002 | 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_003 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
||||||
GF_AutoExport_004 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
GF_AutoExport_004 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
||||||
|
GF_AutoExport_005 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
using GFramework.Godot.SourceGenerators.Diagnostics;
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Behavior;
|
namespace GFramework.Godot.SourceGenerators.Behavior;
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ public sealed class AutoSceneGenerator : IIncrementalGenerator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attribute.ConstructorArguments.Length != 1 || attribute.ConstructorArguments[0].Value is not string key)
|
if (!TryGetSceneKey(context, candidate.TypeSymbol, attribute, out var key))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, key));
|
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, key));
|
||||||
@ -122,6 +121,32 @@ public sealed class AutoSceneGenerator : IIncrementalGenerator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetSceneKey(
|
||||||
|
SourceProductionContext context,
|
||||||
|
INamedTypeSymbol typeSymbol,
|
||||||
|
AttributeData attribute,
|
||||||
|
out string key)
|
||||||
|
{
|
||||||
|
key = string.Empty;
|
||||||
|
|
||||||
|
if (attribute.ConstructorArguments.Length == 1 &&
|
||||||
|
attribute.ConstructorArguments[0].Value is string sceneKey)
|
||||||
|
{
|
||||||
|
key = sceneKey;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
AutoBehaviorDiagnostics.InvalidAttributeArguments,
|
||||||
|
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ??
|
||||||
|
typeSymbol.Locations.FirstOrDefault() ??
|
||||||
|
Location.None,
|
||||||
|
"AutoSceneAttribute",
|
||||||
|
typeSymbol.Name,
|
||||||
|
"a single string scene key argument"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GenerateSource(INamedTypeSymbol typeSymbol, string key)
|
private static string GenerateSource(INamedTypeSymbol typeSymbol, string key)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|||||||
@ -2,10 +2,20 @@ using GFramework.SourceGenerators.Common.Constants;
|
|||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义行为类自动生成器使用的诊断描述符。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 这些规则覆盖 <c>AutoScene</c> 与 <c>AutoUiPage</c> 等行为生成器的常见使用约束,
|
||||||
|
/// 以便在生成被跳过前向调用方报告明确的失败原因。
|
||||||
|
/// </remarks>
|
||||||
internal static class AutoBehaviorDiagnostics
|
internal static class AutoBehaviorDiagnostics
|
||||||
{
|
{
|
||||||
private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Behavior";
|
private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Behavior";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告行为生成器不支持在嵌套类型上运行。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
||||||
"GF_AutoBehavior_001",
|
"GF_AutoBehavior_001",
|
||||||
"Auto behavior generators do not support nested classes",
|
"Auto behavior generators do not support nested classes",
|
||||||
@ -14,6 +24,9 @@ internal static class AutoBehaviorDiagnostics
|
|||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告目标类型没有继承生成器要求的 Godot 基类。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor MissingBaseType = new(
|
public static readonly DiagnosticDescriptor MissingBaseType = new(
|
||||||
"GF_AutoBehavior_002",
|
"GF_AutoBehavior_002",
|
||||||
"Auto behavior generators require a compatible base type",
|
"Auto behavior generators require a compatible base type",
|
||||||
@ -22,6 +35,9 @@ internal static class AutoBehaviorDiagnostics
|
|||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告 UI 页面声明中使用了不存在的 <c>UiLayer</c> 名称。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor InvalidUiLayerName = new(
|
public static readonly DiagnosticDescriptor InvalidUiLayerName = new(
|
||||||
"GF_AutoBehavior_003",
|
"GF_AutoBehavior_003",
|
||||||
"Unknown UiLayer name",
|
"Unknown UiLayer name",
|
||||||
@ -29,4 +45,15 @@ internal static class AutoBehaviorDiagnostics
|
|||||||
Category,
|
Category,
|
||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告行为生成器特性参数不满足约定签名,导致生成器无法推导所需元数据。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor InvalidAttributeArguments = new(
|
||||||
|
"GF_AutoBehavior_004",
|
||||||
|
"Auto behavior attribute arguments are invalid",
|
||||||
|
"Attribute '{0}' on '{1}' must provide {2}",
|
||||||
|
Category,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,20 @@ using GFramework.SourceGenerators.Common.Constants;
|
|||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义导出集合自动注册生成器使用的诊断描述符。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 这些规则用于在源生成阶段验证集合成员、注册目标以及元素类型推导,
|
||||||
|
/// 避免把配置错误延后到生成代码编译或运行时才暴露。
|
||||||
|
/// </remarks>
|
||||||
internal static class AutoRegisterExportedCollectionsDiagnostics
|
internal static class AutoRegisterExportedCollectionsDiagnostics
|
||||||
{
|
{
|
||||||
private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Registration";
|
private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Registration";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告自动注册生成器不支持嵌套类型。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
public static readonly DiagnosticDescriptor NestedClassNotSupported = new(
|
||||||
"GF_AutoExport_001",
|
"GF_AutoExport_001",
|
||||||
"AutoRegisterExportedCollections does not support nested classes",
|
"AutoRegisterExportedCollections does not support nested classes",
|
||||||
@ -14,6 +24,9 @@ internal static class AutoRegisterExportedCollectionsDiagnostics
|
|||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告特性引用的注册表成员在宿主类型上不存在。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor RegistryMemberNotFound = new(
|
public static readonly DiagnosticDescriptor RegistryMemberNotFound = new(
|
||||||
"GF_AutoExport_002",
|
"GF_AutoExport_002",
|
||||||
"Registry member was not found",
|
"Registry member was not found",
|
||||||
@ -22,6 +35,9 @@ internal static class AutoRegisterExportedCollectionsDiagnostics
|
|||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告注册表上未找到与集合元素类型兼容的注册方法。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor RegisterMethodNotFound = new(
|
public static readonly DiagnosticDescriptor RegisterMethodNotFound = new(
|
||||||
"GF_AutoExport_003",
|
"GF_AutoExport_003",
|
||||||
"Register method was not found",
|
"Register method was not found",
|
||||||
@ -30,6 +46,9 @@ internal static class AutoRegisterExportedCollectionsDiagnostics
|
|||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告被标记成员不是可枚举集合,因此无法执行批量注册。
|
||||||
|
/// </summary>
|
||||||
public static readonly DiagnosticDescriptor CollectionTypeMustBeEnumerable = new(
|
public static readonly DiagnosticDescriptor CollectionTypeMustBeEnumerable = new(
|
||||||
"GF_AutoExport_004",
|
"GF_AutoExport_004",
|
||||||
"Exported collection must be enumerable",
|
"Exported collection must be enumerable",
|
||||||
@ -37,4 +56,15 @@ internal static class AutoRegisterExportedCollectionsDiagnostics
|
|||||||
Category,
|
Category,
|
||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 报告集合元素类型无法在编译期推导,因此无法安全匹配注册方法。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor CollectionElementTypeCouldNotBeInferred = new(
|
||||||
|
"GF_AutoExport_005",
|
||||||
|
"Exported collection element type could not be inferred",
|
||||||
|
"Member '{0}' must expose a generic enumerable element type to use RegisterExportedCollection safely",
|
||||||
|
Category,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
using GFramework.Godot.SourceGenerators.Diagnostics;
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Registration;
|
namespace GFramework.Godot.SourceGenerators.Registration;
|
||||||
|
|
||||||
@ -208,12 +207,23 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var elementType = TryGetElementType(collectionType);
|
var elementType = TryGetElementType(collectionType);
|
||||||
|
if (elementType is null)
|
||||||
|
{
|
||||||
|
// Non-generic IEnumerable exposes elements as object at compile time, which is not safe
|
||||||
|
// for validating or generating a strongly typed registry call.
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
AutoRegisterExportedCollectionsDiagnostics.CollectionElementTypeCouldNotBeInferred,
|
||||||
|
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
||||||
|
collectionMember.Name));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var hasCompatibleMethod = registryType.GetMembers(registerMethodName)
|
var hasCompatibleMethod = registryType.GetMembers(registerMethodName)
|
||||||
.OfType<IMethodSymbol>()
|
.OfType<IMethodSymbol>()
|
||||||
.Any(method =>
|
.Any(method =>
|
||||||
!method.IsStatic &&
|
!method.IsStatic &&
|
||||||
method.Parameters.Length == 1 &&
|
method.Parameters.Length == 1 &&
|
||||||
(elementType is null || elementType.IsAssignableTo(method.Parameters[0].Type as INamedTypeSymbol)));
|
elementType.IsAssignableTo(method.Parameters[0].Type as INamedTypeSymbol));
|
||||||
|
|
||||||
if (!hasCompatibleMethod)
|
if (!hasCompatibleMethod)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user