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